actly 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +380 -0
  3. package/dist/core/act.cjs +5 -3
  4. package/dist/core/act.d.ts.map +1 -1
  5. package/dist/core/act.js +4 -2
  6. package/dist/core/act.js.map +1 -1
  7. package/dist/core/executor.cjs +16 -0
  8. package/dist/core/executor.d.ts +9 -7
  9. package/dist/core/executor.d.ts.map +1 -1
  10. package/dist/core/executor.js +15 -0
  11. package/dist/core/executor.js.map +1 -1
  12. package/dist/index.cjs +2 -2
  13. package/dist/index.d.ts +3 -2
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/policies/cache.cjs +27 -7
  18. package/dist/policies/cache.d.ts +5 -0
  19. package/dist/policies/cache.d.ts.map +1 -1
  20. package/dist/policies/cache.js +27 -7
  21. package/dist/policies/cache.js.map +1 -1
  22. package/dist/policies/dedupe.cjs +24 -9
  23. package/dist/policies/dedupe.d.ts +7 -0
  24. package/dist/policies/dedupe.d.ts.map +1 -1
  25. package/dist/policies/dedupe.js +24 -9
  26. package/dist/policies/dedupe.js.map +1 -1
  27. package/dist/state/store.cjs +4 -38
  28. package/dist/state/store.d.ts +1 -12
  29. package/dist/state/store.d.ts.map +1 -1
  30. package/dist/state/store.js +3 -37
  31. package/dist/state/store.js.map +1 -1
  32. package/dist/stores/base.cjs +20 -0
  33. package/dist/stores/base.d.ts +112 -0
  34. package/dist/stores/base.d.ts.map +1 -0
  35. package/dist/stores/base.js +17 -0
  36. package/dist/stores/base.js.map +1 -0
  37. package/dist/stores/memory.cjs +91 -0
  38. package/dist/stores/memory.d.ts +58 -0
  39. package/dist/stores/memory.d.ts.map +1 -0
  40. package/dist/stores/memory.js +88 -0
  41. package/dist/stores/memory.js.map +1 -0
  42. package/dist/types/index.d.ts +20 -8
  43. package/dist/types/index.d.ts.map +1 -1
  44. package/package.json +1 -1
@@ -1,21 +1,41 @@
1
+ import { isSyncStore } from '../stores/base.js';
1
2
  const NS = 'cache:';
2
3
  /**
3
4
  * Short-circuits the entire downstream chain on a cache hit.
4
5
  * On a miss, runs fn and stores the result with TTL.
5
6
  *
6
7
  * Must be the OUTERMOST policy so a hit skips dedupe, timeout, and retry.
8
+ *
9
+ * Supports both SyncStateStore and AsyncStateStore. The async branch
10
+ * introduces two await points (get + set) but has no correctness
11
+ * requirement for same-tick execution — worst case is a cache stampede
12
+ * on a simultaneous miss, which is standard behaviour without locking.
7
13
  */
8
14
  export function cachePolicy(opts) {
9
15
  return (fn, ctx) => async () => {
10
16
  const key = NS + ctx.key;
11
- const hit = ctx.store.get(key);
12
- if (hit) {
13
- ctx.meta.source = 'cache';
14
- return hit.value;
17
+ if (isSyncStore(ctx.store)) {
18
+ // Fast path — synchronous store, no await needed
19
+ const hit = ctx.store.get(key);
20
+ if (hit) {
21
+ ctx.meta.source = 'cache';
22
+ return hit.value;
23
+ }
24
+ const value = await fn();
25
+ ctx.store.set(key, { value }, opts.ttl);
26
+ return value;
27
+ }
28
+ else {
29
+ // Async store path
30
+ const hit = await ctx.store.get(key);
31
+ if (hit) {
32
+ ctx.meta.source = 'cache';
33
+ return hit.value;
34
+ }
35
+ const value = await fn();
36
+ await ctx.store.set(key, { value }, opts.ttl);
37
+ return value;
15
38
  }
16
- const value = await fn();
17
- ctx.store.set(key, { value }, opts.ttl);
18
- return value;
19
39
  };
20
40
  }
21
41
  //# sourceMappingURL=cache.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/policies/cache.ts"],"names":[],"mappings":"AAEA,MAAM,EAAE,GAAG,QAAQ,CAAA;AAOnB;;;;;GAKG;AACH,MAAM,UAAU,WAAW,CAAI,IAAkB;IAC/C,OAAO,CAAC,EAAY,EAAE,GAAkB,EAAY,EAAE,CACpD,KAAK,IAAI,EAAE;QACT,MAAM,GAAG,GAAG,EAAE,GAAG,GAAG,CAAC,GAAG,CAAA;QAExB,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAgB,GAAG,CAAC,CAAA;QAC7C,IAAI,GAAG,EAAE,CAAC;YACR,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAA;YACzB,OAAO,GAAG,CAAC,KAAK,CAAA;QAClB,CAAC;QAED,MAAM,KAAK,GAAG,MAAM,EAAE,EAAE,CAAA;QACxB,GAAG,CAAC,KAAK,CAAC,GAAG,CAAgB,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,CAAA;QACtD,OAAO,KAAK,CAAA;IACd,CAAC,CAAA;AACL,CAAC"}
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/policies/cache.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAE/C,MAAM,EAAE,GAAG,QAAQ,CAAA;AAOnB;;;;;;;;;;GAUG;AACH,MAAM,UAAU,WAAW,CAAI,IAAkB;IAC/C,OAAO,CAAC,EAAY,EAAE,GAAkB,EAAY,EAAE,CACpD,KAAK,IAAI,EAAE;QACT,MAAM,GAAG,GAAG,EAAE,GAAG,GAAG,CAAC,GAAG,CAAA;QAExB,IAAI,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3B,iDAAiD;YACjD,MAAM,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAgB,GAAG,CAAC,CAAA;YAC7C,IAAI,GAAG,EAAE,CAAC;gBACR,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAA;gBACzB,OAAO,GAAG,CAAC,KAAK,CAAA;YAClB,CAAC;YAED,MAAM,KAAK,GAAG,MAAM,EAAE,EAAE,CAAA;YACxB,GAAG,CAAC,KAAK,CAAC,GAAG,CAAgB,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,CAAA;YACtD,OAAO,KAAK,CAAA;QACd,CAAC;aAAM,CAAC;YACN,mBAAmB;YACnB,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,KAAK,CAAC,GAAG,CAAgB,GAAG,CAAC,CAAA;YACnD,IAAI,GAAG,EAAE,CAAC;gBACR,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAA;gBACzB,OAAO,GAAG,CAAC,KAAK,CAAA;YAClB,CAAC;YAED,MAAM,KAAK,GAAG,MAAM,EAAE,EAAE,CAAA;YACxB,MAAM,GAAG,CAAC,KAAK,CAAC,GAAG,CAAgB,GAAG,EAAE,EAAE,KAAK,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,CAAA;YAC5D,OAAO,KAAK,CAAA;QACd,CAAC;IACH,CAAC,CAAA;AACL,CAAC"}
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.dedupePolicy = dedupePolicy;
4
+ const executor_js_1 = require("../core/executor.js");
4
5
  // Namespace so dedupe keys never collide with cache keys in the shared store
5
6
  const NS = 'dedupe:';
6
7
  /**
@@ -9,18 +10,32 @@ const NS = 'dedupe:';
9
10
  * The first caller starts the work. Every subsequent caller that arrives before
10
11
  * the first resolves gets the same Promise back — no duplicate work.
11
12
  *
13
+ * INVARIANT: requires a SyncStateStore — see stores/base.ts.
14
+ * The read-then-write that makes deduplication work must happen in a single
15
+ * synchronous frame. An async store would introduce an await between get() and
16
+ * set(), letting two concurrent callers both see a miss and both launch work.
17
+ * The REQUIRES_SYNC_STORE symbol on the returned PolicyApplier lets execute()
18
+ * enforce this at runtime for JS callers that bypass TypeScript.
19
+ *
12
20
  * Known tradeoff (v1): deduped callers see attempts=1 in their ActResult because
13
21
  * the retry counter belongs to the originating call's meta object.
14
22
  */
15
23
  function dedupePolicy() {
16
- return (fn, ctx) => async () => {
17
- const key = NS + ctx.key;
18
- const inflight = ctx.store.get(key);
19
- if (inflight)
20
- return inflight;
21
- // No TTL .finally() cleans up regardless of outcome
22
- const promise = fn().finally(() => ctx.store.delete(key));
23
- ctx.store.set(key, promise);
24
- return promise;
24
+ const applier = (fn, ctx) => {
25
+ // Cast is safe: execute() verifies isSyncStore(ctx.store) before calling
26
+ // any policy tagged with REQUIRES_SYNC_STORE.
27
+ const syncCtx = ctx;
28
+ return async () => {
29
+ const key = NS + syncCtx.key;
30
+ const inflight = syncCtx.store.get(key);
31
+ if (inflight)
32
+ return inflight;
33
+ // No TTL — .finally() cleans up regardless of outcome
34
+ const promise = fn().finally(() => syncCtx.store.delete(key));
35
+ syncCtx.store.set(key, promise);
36
+ return promise;
37
+ };
25
38
  };
39
+ applier[executor_js_1.REQUIRES_SYNC_STORE] = true;
40
+ return applier;
26
41
  }
@@ -5,6 +5,13 @@ import type { PolicyApplier } from '../types/index.js';
5
5
  * The first caller starts the work. Every subsequent caller that arrives before
6
6
  * the first resolves gets the same Promise back — no duplicate work.
7
7
  *
8
+ * INVARIANT: requires a SyncStateStore — see stores/base.ts.
9
+ * The read-then-write that makes deduplication work must happen in a single
10
+ * synchronous frame. An async store would introduce an await between get() and
11
+ * set(), letting two concurrent callers both see a miss and both launch work.
12
+ * The REQUIRES_SYNC_STORE symbol on the returned PolicyApplier lets execute()
13
+ * enforce this at runtime for JS callers that bypass TypeScript.
14
+ *
8
15
  * Known tradeoff (v1): deduped callers see attempts=1 in their ActResult because
9
16
  * the retry counter belongs to the originating call's meta object.
10
17
  */
@@ -1 +1 @@
1
- {"version":3,"file":"dedupe.d.ts","sourceRoot":"","sources":["../../src/policies/dedupe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,MAAM,mBAAmB,CAAA;AAK5E;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,CAAC,KAAK,aAAa,CAAC,CAAC,CAAC,CAalD"}
1
+ {"version":3,"file":"dedupe.d.ts","sourceRoot":"","sources":["../../src/policies/dedupe.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAS,aAAa,EAAiB,MAAM,mBAAmB,CAAA;AAY5E;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAAC,CAAC,KAAK,aAAa,CAAC,CAAC,CAAC,CAuBlD"}
@@ -1,3 +1,4 @@
1
+ import { REQUIRES_SYNC_STORE } from '../core/executor.js';
1
2
  // Namespace so dedupe keys never collide with cache keys in the shared store
2
3
  const NS = 'dedupe:';
3
4
  /**
@@ -6,19 +7,33 @@ const NS = 'dedupe:';
6
7
  * The first caller starts the work. Every subsequent caller that arrives before
7
8
  * the first resolves gets the same Promise back — no duplicate work.
8
9
  *
10
+ * INVARIANT: requires a SyncStateStore — see stores/base.ts.
11
+ * The read-then-write that makes deduplication work must happen in a single
12
+ * synchronous frame. An async store would introduce an await between get() and
13
+ * set(), letting two concurrent callers both see a miss and both launch work.
14
+ * The REQUIRES_SYNC_STORE symbol on the returned PolicyApplier lets execute()
15
+ * enforce this at runtime for JS callers that bypass TypeScript.
16
+ *
9
17
  * Known tradeoff (v1): deduped callers see attempts=1 in their ActResult because
10
18
  * the retry counter belongs to the originating call's meta object.
11
19
  */
12
20
  export function dedupePolicy() {
13
- return (fn, ctx) => async () => {
14
- const key = NS + ctx.key;
15
- const inflight = ctx.store.get(key);
16
- if (inflight)
17
- return inflight;
18
- // No TTL .finally() cleans up regardless of outcome
19
- const promise = fn().finally(() => ctx.store.delete(key));
20
- ctx.store.set(key, promise);
21
- return promise;
21
+ const applier = (fn, ctx) => {
22
+ // Cast is safe: execute() verifies isSyncStore(ctx.store) before calling
23
+ // any policy tagged with REQUIRES_SYNC_STORE.
24
+ const syncCtx = ctx;
25
+ return async () => {
26
+ const key = NS + syncCtx.key;
27
+ const inflight = syncCtx.store.get(key);
28
+ if (inflight)
29
+ return inflight;
30
+ // No TTL — .finally() cleans up regardless of outcome
31
+ const promise = fn().finally(() => syncCtx.store.delete(key));
32
+ syncCtx.store.set(key, promise);
33
+ return promise;
34
+ };
22
35
  };
36
+ applier[REQUIRES_SYNC_STORE] = true;
37
+ return applier;
23
38
  }
24
39
  //# sourceMappingURL=dedupe.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"dedupe.js","sourceRoot":"","sources":["../../src/policies/dedupe.ts"],"names":[],"mappings":"AAEA,6EAA6E;AAC7E,MAAM,EAAE,GAAG,SAAS,CAAA;AAEpB;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY;IAC1B,OAAO,CAAC,EAAY,EAAE,GAAkB,EAAY,EAAE,CACpD,KAAK,IAAI,EAAE;QACT,MAAM,GAAG,GAAG,EAAE,GAAG,GAAG,CAAC,GAAG,CAAA;QAExB,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,CAAa,GAAG,CAAC,CAAA;QAC/C,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAA;QAE7B,sDAAsD;QACtD,MAAM,OAAO,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;QACzD,GAAG,CAAC,KAAK,CAAC,GAAG,CAAa,GAAG,EAAE,OAAO,CAAC,CAAA;QACvC,OAAO,OAAO,CAAA;IAChB,CAAC,CAAA;AACL,CAAC"}
1
+ {"version":3,"file":"dedupe.js","sourceRoot":"","sources":["../../src/policies/dedupe.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AAEzD,6EAA6E;AAC7E,MAAM,EAAE,GAAG,SAAS,CAAA;AAOpB;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,YAAY;IAC1B,MAAM,OAAO,GAAG,CAAC,EAAY,EAAE,GAAkB,EAAY,EAAE;QAC7D,yEAAyE;QACzE,8CAA8C;QAC9C,MAAM,OAAO,GAAG,GAAoB,CAAA;QAEpC,OAAO,KAAK,IAAI,EAAE;YAChB,MAAM,GAAG,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,CAAA;YAE5B,MAAM,QAAQ,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAa,GAAG,CAAC,CAAA;YACnD,IAAI,QAAQ;gBAAE,OAAO,QAAQ,CAAA;YAE7B,sDAAsD;YACtD,MAAM,OAAO,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAA;YAC7D,OAAO,CAAC,KAAK,CAAC,GAAG,CAAa,GAAG,EAAE,OAAO,CAAC,CAAA;YAC3C,OAAO,OAAO,CAAA;QAChB,CAAC,CAAA;IACH,CAAC,CAGA;IAAC,OAA+D,CAAC,mBAAmB,CAAC,GAAG,IAAI,CAAA;IAE7F,OAAO,OAAO,CAAA;AAChB,CAAC"}
@@ -1,41 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.InMemoryStore = void 0;
4
- class InMemoryStore {
5
- constructor() {
6
- this.entries = new Map();
7
- }
8
- get(key) {
9
- const entry = this.entries.get(key);
10
- if (!entry)
11
- return undefined;
12
- if (entry.expiresAt !== null && Date.now() > entry.expiresAt) {
13
- this.entries.delete(key);
14
- return undefined;
15
- }
16
- return entry.value;
17
- }
18
- set(key, value, ttlMs) {
19
- // ttlMs = 0 or undefined -> no expiry (sentinel null)
20
- const expiresAt = ttlMs != null && ttlMs > 0 ? Date.now() + ttlMs : null;
21
- this.entries.set(key, { value, expiresAt });
22
- }
23
- delete(key) {
24
- this.entries.delete(key);
25
- }
26
- has(key) {
27
- // Reuse get() so expired entries are evicted on access
28
- return this.get(key) !== undefined;
29
- }
30
- /** Drop everything. Handy in tests or for manual cache invalidation. */
31
- clear() {
32
- this.entries.clear();
33
- }
34
- /** Count of live (non-expired) entries. */
35
- get size() {
36
- for (const key of this.entries.keys())
37
- this.has(key);
38
- return this.entries.size;
39
- }
40
- }
41
- exports.InMemoryStore = InMemoryStore;
4
+ // Re-export shim — implementation moved to src/stores/memory.ts in v1.1.
5
+ // Kept for one version to preserve git blame and ease internal refactors.
6
+ var memory_js_1 = require("../stores/memory.js");
7
+ Object.defineProperty(exports, "InMemoryStore", { enumerable: true, get: function () { return memory_js_1.InMemoryStore; } });
@@ -1,13 +1,2 @@
1
- import type { StateStore } from '../types/index.js';
2
- export declare class InMemoryStore implements StateStore {
3
- private entries;
4
- get<T>(key: string): T | undefined;
5
- set<T>(key: string, value: T, ttlMs?: number): void;
6
- delete(key: string): void;
7
- has(key: string): boolean;
8
- /** Drop everything. Handy in tests or for manual cache invalidation. */
9
- clear(): void;
10
- /** Count of live (non-expired) entries. */
11
- get size(): number;
12
- }
1
+ export { InMemoryStore } from '../stores/memory.js';
13
2
  //# sourceMappingURL=store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/state/store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAQnD,qBAAa,aAAc,YAAW,UAAU;IAC9C,OAAO,CAAC,OAAO,CAAoC;IAEnD,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAYlC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI;IAOnD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIzB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAKzB,wEAAwE;IACxE,KAAK,IAAI,IAAI;IAIb,2CAA2C;IAC3C,IAAI,IAAI,IAAI,MAAM,CAGjB;CACF"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/state/store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA"}
@@ -1,38 +1,4 @@
1
- export class InMemoryStore {
2
- constructor() {
3
- this.entries = new Map();
4
- }
5
- get(key) {
6
- const entry = this.entries.get(key);
7
- if (!entry)
8
- return undefined;
9
- if (entry.expiresAt !== null && Date.now() > entry.expiresAt) {
10
- this.entries.delete(key);
11
- return undefined;
12
- }
13
- return entry.value;
14
- }
15
- set(key, value, ttlMs) {
16
- // ttlMs = 0 or undefined -> no expiry (sentinel null)
17
- const expiresAt = ttlMs != null && ttlMs > 0 ? Date.now() + ttlMs : null;
18
- this.entries.set(key, { value, expiresAt });
19
- }
20
- delete(key) {
21
- this.entries.delete(key);
22
- }
23
- has(key) {
24
- // Reuse get() so expired entries are evicted on access
25
- return this.get(key) !== undefined;
26
- }
27
- /** Drop everything. Handy in tests or for manual cache invalidation. */
28
- clear() {
29
- this.entries.clear();
30
- }
31
- /** Count of live (non-expired) entries. */
32
- get size() {
33
- for (const key of this.entries.keys())
34
- this.has(key);
35
- return this.entries.size;
36
- }
37
- }
1
+ // Re-export shim implementation moved to src/stores/memory.ts in v1.1.
2
+ // Kept for one version to preserve git blame and ease internal refactors.
3
+ export { InMemoryStore } from '../stores/memory.js';
38
4
  //# sourceMappingURL=store.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/state/store.ts"],"names":[],"mappings":"AAQA,MAAM,OAAO,aAAa;IAA1B;QACU,YAAO,GAAG,IAAI,GAAG,EAA0B,CAAA;IAwCrD,CAAC;IAtCC,GAAG,CAAI,GAAW;QAChB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACnC,IAAI,CAAC,KAAK;YAAE,OAAO,SAAS,CAAA;QAE5B,IAAI,KAAK,CAAC,SAAS,KAAK,IAAI,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,SAAS,EAAE,CAAC;YAC7D,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;YACxB,OAAO,SAAS,CAAA;QAClB,CAAC;QAED,OAAO,KAAK,CAAC,KAAU,CAAA;IACzB,CAAC;IAED,GAAG,CAAI,GAAW,EAAE,KAAQ,EAAE,KAAc;QAC1C,sDAAsD;QACtD,MAAM,SAAS,GACb,KAAK,IAAI,IAAI,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,IAAI,CAAA;QACxD,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAA;IAC7C,CAAC;IAED,MAAM,CAAC,GAAW;QAChB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IAC1B,CAAC;IAED,GAAG,CAAC,GAAW;QACb,uDAAuD;QACvD,OAAO,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,SAAS,CAAA;IACpC,CAAC;IAED,wEAAwE;IACxE,KAAK;QACH,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAA;IACtB,CAAC;IAED,2CAA2C;IAC3C,IAAI,IAAI;QACN,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE;YAAE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACpD,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAA;IAC1B,CAAC;CACF"}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/state/store.ts"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,0EAA0E;AAC1E,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA"}
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ // ─── Store interfaces ─────────────────────────────────────────────────────────
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.isSyncStore = isSyncStore;
5
+ exports.isAsyncStore = isAsyncStore;
6
+ // ─── Type guards ──────────────────────────────────────────────────────────────
7
+ /**
8
+ * Narrows AnyStateStore to SyncStateStore via the _sync discriminant.
9
+ * Used by execute() and cachePolicy to branch between sync and async paths.
10
+ */
11
+ function isSyncStore(store) {
12
+ return store._sync === true;
13
+ }
14
+ /**
15
+ * Narrows AnyStateStore to AsyncStateStore via the _sync discriminant.
16
+ * Provided for symmetry; prefer isSyncStore for the common guard pattern.
17
+ */
18
+ function isAsyncStore(store) {
19
+ return store._sync === false;
20
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Synchronous key-value store. All operations complete in the same tick.
3
+ *
4
+ * # Why synchronous?
5
+ *
6
+ * dedupePolicy must read an in-flight Promise from the store and, if absent,
7
+ * write a new one — all within a single synchronous frame. If get() were async,
8
+ * two concurrent callers could both observe a miss before either write lands,
9
+ * defeating deduplication entirely. There is no lock primitive in JavaScript
10
+ * that can paper over this: the constraint is structural, not implementation-
11
+ * level.
12
+ *
13
+ * # The _sync discriminant
14
+ *
15
+ * `_sync: true` is a runtime tag that lets execute() enforce the dedupe
16
+ * constraint for plain-JS callers who bypass TypeScript. It is NOT part of the
17
+ * semantic contract and MUST NOT be used for anything beyond that guard.
18
+ * Implementations must set it as a `readonly` literal (`true as const`).
19
+ *
20
+ * # Lifecycle
21
+ *
22
+ * Implementations that hold background resources (timers, connections) should
23
+ * expose a `destroy()` method as a convention, though it is not part of this
24
+ * interface — the async counterpart cannot enforce it symmetrically without
25
+ * requiring Promise returns. See InMemoryStore for the reference pattern.
26
+ *
27
+ * InMemoryStore is the canonical implementation of this interface.
28
+ */
29
+ export interface SyncStateStore {
30
+ /**
31
+ * Runtime discriminant. Read by isSyncStore() in execute() to enforce the
32
+ * dedupe constraint without an instanceof check. Must be `true as const`.
33
+ */
34
+ readonly _sync: true;
35
+ get<T>(key: string): T | undefined;
36
+ set<T>(key: string, value: T, ttlMs?: number): void;
37
+ delete(key: string): void;
38
+ has(key: string): boolean;
39
+ /**
40
+ * Remove all entries synchronously.
41
+ * Must be deterministic: after clear() returns, size() must return 0.
42
+ */
43
+ clear(): void;
44
+ /**
45
+ * Return the count of live (non-expired) entries.
46
+ * Expired entries must not be counted, but implementations are free to
47
+ * evict lazily — the count must reflect only entries observable via get().
48
+ */
49
+ size(): number;
50
+ }
51
+ /**
52
+ * Asynchronous key-value store. All operations return Promises.
53
+ *
54
+ * # Policy compatibility
55
+ *
56
+ * Compatible with cachePolicy only. Passing an AsyncStateStore to a policy
57
+ * chain that includes dedupePolicy is a TypeScript error and a runtime error —
58
+ * execute() will throw at chain-build time. See SyncStateStore for why dedupe
59
+ * requires synchronous access.
60
+ *
61
+ * # TTL semantics
62
+ *
63
+ * The store is responsible for honouring ttlMs. Actly passes it as a hint.
64
+ * Implementations may delegate to a native TTL mechanism (e.g. Redis EXPIRE).
65
+ * There is no enforcement layer above the store boundary.
66
+ *
67
+ * # The _sync discriminant
68
+ *
69
+ * `_sync: false` mirrors the discriminant on SyncStateStore. The executor uses
70
+ * this at runtime to reject async stores in contexts that require synchronous
71
+ * access. Must be set as a `readonly` literal (`false as const`).
72
+ *
73
+ * # Contract parity
74
+ *
75
+ * clear() and size() mirror the SyncStateStore contract so callers building
76
+ * against AnyStateStore can rely on both operations regardless of which variant
77
+ * they receive. Async implementations that do not have a native equivalent
78
+ * (e.g. a bounded Redis namespace) must still satisfy the signature.
79
+ */
80
+ export interface AsyncStateStore {
81
+ /**
82
+ * Runtime discriminant. Must be `false as const`.
83
+ */
84
+ readonly _sync: false;
85
+ get<T>(key: string): Promise<T | undefined>;
86
+ set<T>(key: string, value: T, ttlMs?: number): Promise<void>;
87
+ delete(key: string): Promise<void>;
88
+ has(key: string): Promise<boolean>;
89
+ /**
90
+ * Remove all entries managed by this store.
91
+ * For scoped adapters (e.g. a Redis key-prefix namespace), remove only the
92
+ * entries owned by this instance — not the entire backing store.
93
+ */
94
+ clear(): Promise<void>;
95
+ /**
96
+ * Return the count of live (non-expired) entries.
97
+ * Expired entries must not be counted. For eventually-consistent backends,
98
+ * the count is a best-effort snapshot at the time of the call.
99
+ */
100
+ size(): Promise<number>;
101
+ }
102
+ /**
103
+ * Narrows AnyStateStore to SyncStateStore via the _sync discriminant.
104
+ * Used by execute() and cachePolicy to branch between sync and async paths.
105
+ */
106
+ export declare function isSyncStore(store: SyncStateStore | AsyncStateStore): store is SyncStateStore;
107
+ /**
108
+ * Narrows AnyStateStore to AsyncStateStore via the _sync discriminant.
109
+ * Provided for symmetry; prefer isSyncStore for the common guard pattern.
110
+ */
111
+ export declare function isAsyncStore(store: SyncStateStore | AsyncStateStore): store is AsyncStateStore;
112
+ //# sourceMappingURL=base.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base.d.ts","sourceRoot":"","sources":["../../src/stores/base.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAA;IAEpB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAAA;IAClC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;IACnD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAA;IAEzB;;;OAGG;IACH,KAAK,IAAI,IAAI,CAAA;IAEb;;;;OAIG;IACH,IAAI,IAAI,MAAM,CAAA;CACf;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AACH,MAAM,WAAW,eAAe;IAC9B;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,KAAK,CAAA;IAErB,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC,CAAA;IAC3C,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAC5D,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAElC;;;;OAIG;IACH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;IAEtB;;;;OAIG;IACH,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;CACxB;AAID;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,cAAc,GAAG,eAAe,GAAG,KAAK,IAAI,cAAc,CAE5F;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,cAAc,GAAG,eAAe,GAAG,KAAK,IAAI,eAAe,CAE9F"}
@@ -0,0 +1,17 @@
1
+ // ─── Store interfaces ─────────────────────────────────────────────────────────
2
+ // ─── Type guards ──────────────────────────────────────────────────────────────
3
+ /**
4
+ * Narrows AnyStateStore to SyncStateStore via the _sync discriminant.
5
+ * Used by execute() and cachePolicy to branch between sync and async paths.
6
+ */
7
+ export function isSyncStore(store) {
8
+ return store._sync === true;
9
+ }
10
+ /**
11
+ * Narrows AnyStateStore to AsyncStateStore via the _sync discriminant.
12
+ * Provided for symmetry; prefer isSyncStore for the common guard pattern.
13
+ */
14
+ export function isAsyncStore(store) {
15
+ return store._sync === false;
16
+ }
17
+ //# sourceMappingURL=base.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"base.js","sourceRoot":"","sources":["../../src/stores/base.ts"],"names":[],"mappings":"AAAA,iFAAiF;AA+GjF,iFAAiF;AAEjF;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,KAAuC;IACjE,OAAO,KAAK,CAAC,KAAK,KAAK,IAAI,CAAA;AAC7B,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,KAAuC;IAClE,OAAO,KAAK,CAAC,KAAK,KAAK,KAAK,CAAA;AAC9B,CAAC"}
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.InMemoryStore = void 0;
4
+ function isUnrefable(t) {
5
+ return typeof t.unref === 'function';
6
+ }
7
+ // ─── Implementation ───────────────────────────────────────────────────────────
8
+ class InMemoryStore {
9
+ constructor(options = {}) {
10
+ // Discriminant read by isSyncStore() in execute() to enforce the dedupe
11
+ // constraint at runtime for JS callers who bypass TypeScript.
12
+ this._sync = true;
13
+ this.entries = new Map();
14
+ const { autoCleanup = false, cleanupIntervalMs = 30000 } = options;
15
+ if (autoCleanup) {
16
+ const timer = setInterval(() => this._sweep(), cleanupIntervalMs);
17
+ // Prevent the interval from keeping the Node.js process alive when the
18
+ // application has otherwise finished its work. Safe no-op in browsers.
19
+ if (isUnrefable(timer))
20
+ timer.unref();
21
+ this.cleanupTimer = timer;
22
+ }
23
+ }
24
+ get(key) {
25
+ const entry = this.entries.get(key);
26
+ if (!entry)
27
+ return undefined;
28
+ if (entry.expiresAt !== null && Date.now() > entry.expiresAt) {
29
+ this.entries.delete(key);
30
+ return undefined;
31
+ }
32
+ return entry.value;
33
+ }
34
+ set(key, value, ttlMs) {
35
+ // ttlMs = 0 or undefined → no expiry (sentinel null)
36
+ const expiresAt = ttlMs != null && ttlMs > 0 ? Date.now() + ttlMs : null;
37
+ this.entries.set(key, { value, expiresAt });
38
+ }
39
+ delete(key) {
40
+ this.entries.delete(key);
41
+ }
42
+ has(key) {
43
+ // Delegate to get() so expired entries are evicted on access.
44
+ return this.get(key) !== undefined;
45
+ }
46
+ /**
47
+ * Remove all entries.
48
+ * After this call, size() returns 0.
49
+ */
50
+ clear() {
51
+ this.entries.clear();
52
+ }
53
+ /**
54
+ * Return the count of live (non-expired) entries.
55
+ *
56
+ * Expired entries are evicted during the scan, so repeated calls are
57
+ * slightly cheaper as the map self-prunes. O(n) in the number of entries.
58
+ */
59
+ size() {
60
+ // Evict expired entries as we scan — keeps the map tidy between sweeps
61
+ // and ensures the returned count reflects only observable entries.
62
+ for (const key of this.entries.keys())
63
+ this.has(key);
64
+ return this.entries.size;
65
+ }
66
+ /**
67
+ * Stop the background cleanup timer and release internal state.
68
+ * Safe to call multiple times — subsequent calls are no-ops.
69
+ *
70
+ * Call destroy() when discarding a long-lived store instance to prevent
71
+ * timer leaks. Stores without autoCleanup enabled have nothing to release,
72
+ * but destroy() is safe to call on them regardless.
73
+ */
74
+ destroy() {
75
+ if (this.cleanupTimer !== undefined) {
76
+ clearInterval(this.cleanupTimer);
77
+ this.cleanupTimer = undefined;
78
+ }
79
+ }
80
+ // Sweep all entries and remove those past their expiry time.
81
+ // Called by the autoCleanup interval — not part of the public contract.
82
+ _sweep() {
83
+ const now = Date.now();
84
+ for (const [key, entry] of this.entries) {
85
+ if (entry.expiresAt !== null && now > entry.expiresAt) {
86
+ this.entries.delete(key);
87
+ }
88
+ }
89
+ }
90
+ }
91
+ exports.InMemoryStore = InMemoryStore;
@@ -0,0 +1,58 @@
1
+ import type { SyncStateStore } from './base.js';
2
+ export interface InMemoryStoreOptions {
3
+ /**
4
+ * Periodically sweep and remove expired entries in the background.
5
+ *
6
+ * Disabled by default. The store evicts lazily on get()/has() access, which
7
+ * is sufficient for most use cases. Enable autoCleanup when the store is
8
+ * long-lived and accumulates many TTL'd entries that are never re-read — for
9
+ * example, a server-side cache that receives write-heavy traffic with low
10
+ * subsequent read rates.
11
+ *
12
+ * The sweep does not affect observable store semantics: expired entries are
13
+ * already invisible to get()/has()/size() before the sweep runs.
14
+ */
15
+ autoCleanup?: boolean;
16
+ /**
17
+ * Interval between background sweeps in milliseconds.
18
+ * Defaults to 30 000 (30 seconds). Ignored when autoCleanup is false.
19
+ *
20
+ * Choose a value appropriate to your TTL distribution — sweeping more often
21
+ * than your shortest TTL is wasteful; sweeping much less often than your
22
+ * longest TTL wastes memory.
23
+ */
24
+ cleanupIntervalMs?: number;
25
+ }
26
+ export declare class InMemoryStore implements SyncStateStore {
27
+ readonly _sync: true;
28
+ private readonly entries;
29
+ private cleanupTimer;
30
+ constructor(options?: InMemoryStoreOptions);
31
+ get<T>(key: string): T | undefined;
32
+ set<T>(key: string, value: T, ttlMs?: number): void;
33
+ delete(key: string): void;
34
+ has(key: string): boolean;
35
+ /**
36
+ * Remove all entries.
37
+ * After this call, size() returns 0.
38
+ */
39
+ clear(): void;
40
+ /**
41
+ * Return the count of live (non-expired) entries.
42
+ *
43
+ * Expired entries are evicted during the scan, so repeated calls are
44
+ * slightly cheaper as the map self-prunes. O(n) in the number of entries.
45
+ */
46
+ size(): number;
47
+ /**
48
+ * Stop the background cleanup timer and release internal state.
49
+ * Safe to call multiple times — subsequent calls are no-ops.
50
+ *
51
+ * Call destroy() when discarding a long-lived store instance to prevent
52
+ * timer leaks. Stores without autoCleanup enabled have nothing to release,
53
+ * but destroy() is safe to call on them regardless.
54
+ */
55
+ destroy(): void;
56
+ private _sweep;
57
+ }
58
+ //# sourceMappingURL=memory.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/stores/memory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAuB/C,MAAM,WAAW,oBAAoB;IACnC;;;;;;;;;;;OAWG;IACH,WAAW,CAAC,EAAE,OAAO,CAAA;IAErB;;;;;;;OAOG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAC3B;AAID,qBAAa,aAAc,YAAW,cAAc;IAGlD,QAAQ,CAAC,KAAK,EAAG,IAAI,CAAS;IAE9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAoC;IAC5D,OAAO,CAAC,YAAY,CAA4C;gBAEpD,OAAO,GAAE,oBAAyB;IAY9C,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAYlC,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI;IAMnD,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIzB,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAKzB;;;OAGG;IACH,KAAK,IAAI,IAAI;IAIb;;;;;OAKG;IACH,IAAI,IAAI,MAAM;IAOd;;;;;;;OAOG;IACH,OAAO,IAAI,IAAI;IASf,OAAO,CAAC,MAAM;CAQf"}