actly 1.0.2 → 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.
- package/LICENSE +1 -1
- package/README.md +380 -0
- package/dist/core/act.cjs +5 -3
- package/dist/core/act.d.ts.map +1 -1
- package/dist/core/act.js +4 -2
- package/dist/core/act.js.map +1 -1
- package/dist/core/executor.cjs +16 -0
- package/dist/core/executor.d.ts +9 -7
- package/dist/core/executor.d.ts.map +1 -1
- package/dist/core/executor.js +15 -0
- package/dist/core/executor.js.map +1 -1
- package/dist/index.cjs +2 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/policies/cache.cjs +27 -7
- package/dist/policies/cache.d.ts +5 -0
- package/dist/policies/cache.d.ts.map +1 -1
- package/dist/policies/cache.js +27 -7
- package/dist/policies/cache.js.map +1 -1
- package/dist/policies/dedupe.cjs +24 -9
- package/dist/policies/dedupe.d.ts +7 -0
- package/dist/policies/dedupe.d.ts.map +1 -1
- package/dist/policies/dedupe.js +24 -9
- package/dist/policies/dedupe.js.map +1 -1
- package/dist/state/store.cjs +4 -38
- package/dist/state/store.d.ts +1 -12
- package/dist/state/store.d.ts.map +1 -1
- package/dist/state/store.js +3 -37
- package/dist/state/store.js.map +1 -1
- package/dist/stores/base.cjs +20 -0
- package/dist/stores/base.d.ts +112 -0
- package/dist/stores/base.d.ts.map +1 -0
- package/dist/stores/base.js +17 -0
- package/dist/stores/base.js.map +1 -0
- package/dist/stores/memory.cjs +91 -0
- package/dist/stores/memory.d.ts +58 -0
- package/dist/stores/memory.d.ts.map +1 -0
- package/dist/stores/memory.js +88 -0
- package/dist/stores/memory.js.map +1 -0
- package/dist/types/index.d.ts +20 -8
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/policies/cache.js
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
ctx.
|
|
14
|
-
|
|
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":"
|
|
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"}
|
package/dist/policies/dedupe.cjs
CHANGED
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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;
|
|
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"}
|
package/dist/policies/dedupe.js
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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;
|
|
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"}
|
package/dist/state/store.cjs
CHANGED
|
@@ -1,41 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.InMemoryStore = void 0;
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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; } });
|
package/dist/state/store.d.ts
CHANGED
|
@@ -1,13 +1,2 @@
|
|
|
1
|
-
|
|
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":"
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/state/store.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA"}
|
package/dist/state/store.js
CHANGED
|
@@ -1,38 +1,4 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
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
|
package/dist/state/store.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.js","sourceRoot":"","sources":["../../src/state/store.ts"],"names":[],"mappings":"
|
|
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"}
|