fix(rete): inject clock into EventLog; use tsc for DTS; fix cycle.test.ts private access; add Playwright worker limit
This commit is contained in:
parent
103f2bd0a6
commit
ca6a3df500
5 changed files with 71 additions and 63 deletions
|
|
@ -13,7 +13,7 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
"build": "tsup src/index.ts --format esm,cjs --clean && tsc -p tsconfig.build.json --emitDeclarationOnly --noEmitOnError",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
|
||||||
|
|
@ -33,87 +33,77 @@ describe("RecursionLimitExceededError", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Session.fireRules() recursion limit", () => {
|
describe("Session.fireRules() recursion limit", () => {
|
||||||
it("throws RecursionLimitExceededError when recursion depth exceeds limit", () => {
|
it("throws RecursionLimitExceededError when recursion depth exceeds limit", () => {
|
||||||
const session = new Session({ autoFire: false, recursionLimit: 3 });
|
const session = new Session({ autoFire: false, recursionLimit: 3 });
|
||||||
expect(() => {
|
expect(() => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
session._fireDepth = 4;
|
||||||
(session as any)._fireDepth = 4;
|
|
||||||
session.fireRules({ recursionLimit: 3 });
|
session.fireRules({ recursionLimit: 3 });
|
||||||
}).toThrow(RecursionLimitExceededError);
|
}).toThrow(RecursionLimitExceededError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws when depth equals limit (>=)", () => {
|
it("throws when depth equals limit (>=)", () => {
|
||||||
const session = new Session({ autoFire: false, recursionLimit: 3 });
|
const session = new Session({ autoFire: false, recursionLimit: 3 });
|
||||||
expect(() => {
|
expect(() => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
session._fireDepth = 3;
|
||||||
(session as any)._fireDepth = 3;
|
|
||||||
session.fireRules({ recursionLimit: 3 });
|
session.fireRules({ recursionLimit: 3 });
|
||||||
}).toThrow(RecursionLimitExceededError);
|
}).toThrow(RecursionLimitExceededError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not throw when depth is below limit", () => {
|
it("does not throw when depth is below limit", () => {
|
||||||
const session = new Session({ autoFire: false, recursionLimit: 64 });
|
const session = new Session({ autoFire: false, recursionLimit: 64 });
|
||||||
expect(() => {
|
expect(() => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
session._fireDepth = 0;
|
||||||
(session as any)._fireDepth = 0;
|
|
||||||
session.fireRules({ recursionLimit: 64 });
|
session.fireRules({ recursionLimit: 64 });
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses session default recursionLimit when opts omitted", () => {
|
it("uses session default recursionLimit when opts omitted", () => {
|
||||||
const session = new Session({ autoFire: false, recursionLimit: 3 });
|
const session = new Session({ autoFire: false, recursionLimit: 3 });
|
||||||
expect(() => {
|
expect(() => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
session._fireDepth = 5;
|
||||||
(session as any)._fireDepth = 5;
|
|
||||||
session.fireRules();
|
session.fireRules();
|
||||||
}).toThrow(RecursionLimitExceededError);
|
}).toThrow(RecursionLimitExceededError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("resets recursion depth to 0 between independent fireRules() calls", () => {
|
it("resets recursion depth to 0 between independent fireRules() calls", () => {
|
||||||
const session = new Session({ autoFire: false, recursionLimit: 3 });
|
const session = new Session({ autoFire: false, recursionLimit: 3 });
|
||||||
expect(() => session.fireRules()).not.toThrow();
|
expect(() => session.fireRules()).not.toThrow();
|
||||||
expect(() => session.fireRules()).not.toThrow();
|
expect(() => session.fireRules()).not.toThrow();
|
||||||
expect(() => session.fireRules()).not.toThrow();
|
expect(() => session.fireRules()).not.toThrow();
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
expect(session._fireDepth).toBe(0);
|
||||||
expect((session as any)._fireDepth).toBe(0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("decrements depth even if fireRules body were to throw (finally)", () => {
|
it("decrements depth even if fireRules body were to throw (finally)", () => {
|
||||||
const session = new Session({ autoFire: false, recursionLimit: 3 });
|
const session = new Session({ autoFire: false, recursionLimit: 3 });
|
||||||
// Force recursion guard to throw; internal counter must not be left elevated
|
// Force recursion guard to throw; internal counter must not be left elevated
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
session._fireDepth = 5;
|
||||||
(session as any)._fireDepth = 5;
|
expect(() => session.fireRules({ recursionLimit: 3 })).toThrow(
|
||||||
expect(() => session.fireRules({ recursionLimit: 3 })).toThrow(
|
RecursionLimitExceededError,
|
||||||
RecursionLimitExceededError,
|
);
|
||||||
);
|
// After throw, depth should still be 5 (guard throws before increment)
|
||||||
// After throw, depth should still be 5 (guard throws before increment)
|
expect(session._fireDepth).toBe(5);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
expect((session as any)._fireDepth).toBe(5);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("recursionLimit: 0 means no limit — does not throw", () => {
|
it("recursionLimit: 0 means no limit — does not throw", () => {
|
||||||
const session = new Session({ autoFire: false, recursionLimit: 0 });
|
const session = new Session({ autoFire: false, recursionLimit: 0 });
|
||||||
expect(() => {
|
expect(() => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
session._fireDepth = 1000;
|
||||||
(session as any)._fireDepth = 1000;
|
|
||||||
session.fireRules({ recursionLimit: 0 });
|
session.fireRules({ recursionLimit: 0 });
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("recursionLimit: 0 at session level also means no limit", () => {
|
it("recursionLimit: 0 at session level also means no limit", () => {
|
||||||
const session = new Session({ autoFire: false, recursionLimit: 0 });
|
const session = new Session({ autoFire: false, recursionLimit: 0 });
|
||||||
expect(() => {
|
expect(() => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
session._fireDepth = 1000;
|
||||||
(session as any)._fireDepth = 1000;
|
|
||||||
session.fireRules();
|
session.fireRules();
|
||||||
}).not.toThrow();
|
}).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("thrown error carries the actual depth value", () => {
|
it("thrown error carries the actual depth value", () => {
|
||||||
const session = new Session({ autoFire: false, recursionLimit: 3 });
|
const session = new Session({ autoFire: false, recursionLimit: 3 });
|
||||||
try {
|
try {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
session._fireDepth = 7;
|
||||||
(session as any)._fireDepth = 7;
|
|
||||||
session.fireRules({ recursionLimit: 3 });
|
session.fireRules({ recursionLimit: 3 });
|
||||||
expect.fail("should have thrown");
|
expect.fail("should have thrown");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -122,12 +112,11 @@ describe("Session.fireRules() recursion limit", () => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opts.recursionLimit overrides the session default", () => {
|
it("opts.recursionLimit overrides the session default", () => {
|
||||||
const session = new Session({ autoFire: false, recursionLimit: 64 });
|
const session = new Session({ autoFire: false, recursionLimit: 64 });
|
||||||
// Session default is 64 — would not throw. But opts override to 2.
|
// Session default is 64 — would not throw. But opts override to 2.
|
||||||
expect(() => {
|
expect(() => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
session._fireDepth = 2;
|
||||||
(session as any)._fireDepth = 2;
|
|
||||||
session.fireRules({ recursionLimit: 2 });
|
session.fireRules({ recursionLimit: 2 });
|
||||||
}).toThrow(RecursionLimitExceededError);
|
}).toThrow(RecursionLimitExceededError);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,16 @@ export interface LogEvent {
|
||||||
export class EventLog {
|
export class EventLog {
|
||||||
readonly #events: LogEvent[] = [];
|
readonly #events: LogEvent[] = [];
|
||||||
#seq = 0;
|
#seq = 0;
|
||||||
|
readonly #clock: () => number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param clock — Optional wall-clock function. Defaults to `Date.now`.
|
||||||
|
* Inject a deterministic clock (e.g. `() => 0`) in tests and replay
|
||||||
|
* contexts where timestamp values must be stable across runs.
|
||||||
|
*/
|
||||||
|
constructor(clock: () => number = Date.now) {
|
||||||
|
this.#clock = clock;
|
||||||
|
}
|
||||||
|
|
||||||
/** Number of events logged. */
|
/** Number of events logged. */
|
||||||
get length(): number {
|
get length(): number {
|
||||||
|
|
@ -67,7 +77,7 @@ export class EventLog {
|
||||||
appendInsert(id: EntityId, attr: AttrKey, value: FactValue): void {
|
appendInsert(id: EntityId, attr: AttrKey, value: FactValue): void {
|
||||||
this.#events.push({
|
this.#events.push({
|
||||||
seq: ++this.#seq,
|
seq: ++this.#seq,
|
||||||
ts: Date.now(),
|
ts: this.#clock(),
|
||||||
kind: "insert",
|
kind: "insert",
|
||||||
id,
|
id,
|
||||||
attr,
|
attr,
|
||||||
|
|
@ -82,7 +92,7 @@ export class EventLog {
|
||||||
appendRetract(id: EntityId, attr: AttrKey, value: FactValue): void {
|
appendRetract(id: EntityId, attr: AttrKey, value: FactValue): void {
|
||||||
this.#events.push({
|
this.#events.push({
|
||||||
seq: ++this.#seq,
|
seq: ++this.#seq,
|
||||||
ts: Date.now(),
|
ts: this.#clock(),
|
||||||
kind: "retract",
|
kind: "retract",
|
||||||
id,
|
id,
|
||||||
attr,
|
attr,
|
||||||
|
|
@ -94,7 +104,7 @@ export class EventLog {
|
||||||
appendFire(ruleName: string): void {
|
appendFire(ruleName: string): void {
|
||||||
this.#events.push({
|
this.#events.push({
|
||||||
seq: ++this.#seq,
|
seq: ++this.#seq,
|
||||||
ts: Date.now(),
|
ts: this.#clock(),
|
||||||
kind: "fire",
|
kind: "fire",
|
||||||
ruleName,
|
ruleName,
|
||||||
});
|
});
|
||||||
|
|
|
||||||
8
packages/rete/tsconfig.build.json
Normal file
8
packages/rete/tsconfig.build.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": false,
|
||||||
|
"incremental": false,
|
||||||
|
"declarationMap": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,4 +15,5 @@ export default defineConfig({
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
},
|
},
|
||||||
reporter: [["html", { open: "never" }], ["list"]],
|
reporter: [["html", { open: "never" }], ["list"]],
|
||||||
|
workers: 1, // prevent parallel workers from sharing the single dev server
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue