diff --git a/packages/rete/package.json b/packages/rete/package.json index b349593..9b0121a 100644 --- a/packages/rete/package.json +++ b/packages/rete/package.json @@ -13,7 +13,7 @@ } }, "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" }, "dependencies": { diff --git a/packages/rete/src/cycle.test.ts b/packages/rete/src/cycle.test.ts index ba8477f..9e9b042 100644 --- a/packages/rete/src/cycle.test.ts +++ b/packages/rete/src/cycle.test.ts @@ -33,87 +33,77 @@ describe("RecursionLimitExceededError", () => { }); describe("Session.fireRules() recursion limit", () => { - it("throws RecursionLimitExceededError when recursion depth exceeds limit", () => { - const session = new Session({ autoFire: false, recursionLimit: 3 }); - expect(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (session as any)._fireDepth = 4; + it("throws RecursionLimitExceededError when recursion depth exceeds limit", () => { + const session = new Session({ autoFire: false, recursionLimit: 3 }); + expect(() => { + session._fireDepth = 4; session.fireRules({ recursionLimit: 3 }); }).toThrow(RecursionLimitExceededError); }); - it("throws when depth equals limit (>=)", () => { - const session = new Session({ autoFire: false, recursionLimit: 3 }); - expect(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (session as any)._fireDepth = 3; + it("throws when depth equals limit (>=)", () => { + const session = new Session({ autoFire: false, recursionLimit: 3 }); + expect(() => { + session._fireDepth = 3; session.fireRules({ recursionLimit: 3 }); }).toThrow(RecursionLimitExceededError); }); - it("does not throw when depth is below limit", () => { - const session = new Session({ autoFire: false, recursionLimit: 64 }); - expect(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (session as any)._fireDepth = 0; + it("does not throw when depth is below limit", () => { + const session = new Session({ autoFire: false, recursionLimit: 64 }); + expect(() => { + session._fireDepth = 0; session.fireRules({ recursionLimit: 64 }); }).not.toThrow(); }); - it("uses session default recursionLimit when opts omitted", () => { - const session = new Session({ autoFire: false, recursionLimit: 3 }); - expect(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (session as any)._fireDepth = 5; + it("uses session default recursionLimit when opts omitted", () => { + const session = new Session({ autoFire: false, recursionLimit: 3 }); + expect(() => { + session._fireDepth = 5; session.fireRules(); }).toThrow(RecursionLimitExceededError); }); - it("resets recursion depth to 0 between independent fireRules() calls", () => { - const session = new Session({ autoFire: false, recursionLimit: 3 }); - 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 as any)._fireDepth).toBe(0); + it("resets recursion depth to 0 between independent fireRules() calls", () => { + 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._fireDepth).toBe(0); }); - it("decrements depth even if fireRules body were to throw (finally)", () => { - const session = new Session({ autoFire: false, recursionLimit: 3 }); - // Force recursion guard to throw; internal counter must not be left elevated - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (session as any)._fireDepth = 5; - expect(() => session.fireRules({ recursionLimit: 3 })).toThrow( - RecursionLimitExceededError, - ); - // After throw, depth should still be 5 (guard throws before increment) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - expect((session as any)._fireDepth).toBe(5); + it("decrements depth even if fireRules body were to throw (finally)", () => { + const session = new Session({ autoFire: false, recursionLimit: 3 }); + // Force recursion guard to throw; internal counter must not be left elevated + session._fireDepth = 5; + expect(() => session.fireRules({ recursionLimit: 3 })).toThrow( + RecursionLimitExceededError, + ); + // After throw, depth should still be 5 (guard throws before increment) + expect(session._fireDepth).toBe(5); }); - it("recursionLimit: 0 means no limit — does not throw", () => { - const session = new Session({ autoFire: false, recursionLimit: 0 }); - expect(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (session as any)._fireDepth = 1000; + it("recursionLimit: 0 means no limit — does not throw", () => { + const session = new Session({ autoFire: false, recursionLimit: 0 }); + expect(() => { + session._fireDepth = 1000; session.fireRules({ recursionLimit: 0 }); }).not.toThrow(); }); - it("recursionLimit: 0 at session level also means no limit", () => { - const session = new Session({ autoFire: false, recursionLimit: 0 }); - expect(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (session as any)._fireDepth = 1000; + it("recursionLimit: 0 at session level also means no limit", () => { + const session = new Session({ autoFire: false, recursionLimit: 0 }); + expect(() => { + session._fireDepth = 1000; session.fireRules(); }).not.toThrow(); }); - it("thrown error carries the actual depth value", () => { - const session = new Session({ autoFire: false, recursionLimit: 3 }); - try { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (session as any)._fireDepth = 7; + it("thrown error carries the actual depth value", () => { + const session = new Session({ autoFire: false, recursionLimit: 3 }); + try { + session._fireDepth = 7; session.fireRules({ recursionLimit: 3 }); expect.fail("should have thrown"); } catch (err) { @@ -122,12 +112,11 @@ describe("Session.fireRules() recursion limit", () => { } }); - it("opts.recursionLimit overrides the session default", () => { - const session = new Session({ autoFire: false, recursionLimit: 64 }); - // Session default is 64 — would not throw. But opts override to 2. - expect(() => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (session as any)._fireDepth = 2; + it("opts.recursionLimit overrides the session default", () => { + const session = new Session({ autoFire: false, recursionLimit: 64 }); + // Session default is 64 — would not throw. But opts override to 2. + expect(() => { + session._fireDepth = 2; session.fireRules({ recursionLimit: 2 }); }).toThrow(RecursionLimitExceededError); }); diff --git a/packages/rete/src/eventlog.ts b/packages/rete/src/eventlog.ts index 21c1054..f0d969e 100644 --- a/packages/rete/src/eventlog.ts +++ b/packages/rete/src/eventlog.ts @@ -48,6 +48,16 @@ export interface LogEvent { export class EventLog { readonly #events: LogEvent[] = []; #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. */ get length(): number { @@ -67,7 +77,7 @@ export class EventLog { appendInsert(id: EntityId, attr: AttrKey, value: FactValue): void { this.#events.push({ seq: ++this.#seq, - ts: Date.now(), + ts: this.#clock(), kind: "insert", id, attr, @@ -82,7 +92,7 @@ export class EventLog { appendRetract(id: EntityId, attr: AttrKey, value: FactValue): void { this.#events.push({ seq: ++this.#seq, - ts: Date.now(), + ts: this.#clock(), kind: "retract", id, attr, @@ -94,7 +104,7 @@ export class EventLog { appendFire(ruleName: string): void { this.#events.push({ seq: ++this.#seq, - ts: Date.now(), + ts: this.#clock(), kind: "fire", ruleName, }); diff --git a/packages/rete/tsconfig.build.json b/packages/rete/tsconfig.build.json new file mode 100644 index 0000000..a194452 --- /dev/null +++ b/packages/rete/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "composite": false, + "incremental": false, + "declarationMap": false + } +} diff --git a/playwright.config.ts b/playwright.config.ts index ce984f2..3778a71 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -15,4 +15,5 @@ export default defineConfig({ timeout: 30000, }, reporter: [["html", { open: "never" }], ["list"]], + workers: 1, // prevent parallel workers from sharing the single dev server });