replyonce 0.0.0-stage → 0.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 +202 -0
- package/NOTICE +7 -0
- package/README.md +327 -2
- package/dist/adapters.cjs +260 -0
- package/dist/adapters.cjs.map +1 -0
- package/dist/adapters.d.cts +134 -0
- package/dist/adapters.d.ts +134 -0
- package/dist/adapters.js +224 -0
- package/dist/adapters.js.map +1 -0
- package/dist/cli.js +999 -0
- package/dist/index.cjs +839 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +27 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +801 -0
- package/dist/index.js.map +1 -0
- package/dist/postgres.cjs +146 -0
- package/dist/postgres.cjs.map +1 -0
- package/dist/postgres.d.cts +48 -0
- package/dist/postgres.d.ts +48 -0
- package/dist/postgres.js +120 -0
- package/dist/postgres.js.map +1 -0
- package/dist/redis.cjs +85 -0
- package/dist/redis.cjs.map +1 -0
- package/dist/redis.d.cts +40 -0
- package/dist/redis.d.ts +40 -0
- package/dist/redis.js +59 -0
- package/dist/redis.js.map +1 -0
- package/dist/replyonce-CGwSjxB2.d.cts +209 -0
- package/dist/replyonce-DNRS4FWY.d.ts +209 -0
- package/dist/types-Dn7TSEnu.d.cts +209 -0
- package/dist/types-Dn7TSEnu.d.ts +209 -0
- package/package.json +107 -4
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stores/redis.ts"],"sourcesContent":["import type { ScopeState, Store, TransactResult } from '../types.js';\n\n/**\n * The one Redis command replyonce uses, in ioredis' signature. For node-redis, pass\n * `{ eval: (script, n, ...args) => client.eval(script, { keys: args.slice(0, n), arguments: args.slice(n) }) }`.\n */\nexport interface RedisLike {\n eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n}\n\nexport interface RedisStoreOptions {\n client: RedisLike;\n /** Key prefix. Default `replyonce`. */\n prefix?: string;\n /** Expire idle scopes after this long. 0 disables expiry. Default 30 days. */\n keyTtlMs?: number;\n /** Optimistic retries under contention before giving up. Default 200. */\n maxRetries?: number;\n}\n\nconst READ = `return redis.call('HMGET', KEYS[1], 's', 'v')`;\n\nconst CAS = `\nlocal v = tonumber(redis.call('HGET', KEYS[1], 'v') or '0')\nif v ~= tonumber(ARGV[1]) then return 0 end\nredis.call('HSET', KEYS[1], 's', ARGV[2], 'v', v + 1)\nlocal ttl = tonumber(ARGV[3])\nif ttl > 0 then redis.call('PEXPIRE', KEYS[1], ttl) end\nreturn 1\n`;\n\nexport class StoreContentionError extends Error {\n constructor(scope: string, attempts: number) {\n super(`replyonce: gave up on scope ${JSON.stringify(scope)} after ${attempts} contended attempts`);\n this.name = 'StoreContentionError';\n }\n}\n\n/**\n * Redis store. Each scope is one hash holding the state and a version. `transact` reads\n * both, decides locally, then writes with a compare-and-set Lua script, retrying on a lost\n * race. Works on a shared connection (no WATCH) and with Redis Cluster (one key per op).\n */\nexport class RedisStore implements Store {\n private readonly client: RedisLike;\n private readonly prefix: string;\n private readonly keyTtlMs: number;\n private readonly maxRetries: number;\n\n constructor(options: RedisStoreOptions) {\n if (!options?.client) throw new TypeError('replyonce: RedisStore needs a client');\n this.client = options.client;\n this.prefix = options.prefix ?? 'replyonce';\n this.keyTtlMs = options.keyTtlMs ?? 30 * 24 * 3_600_000;\n this.maxRetries = options.maxRetries ?? 200;\n }\n\n private redisKey(scope: string): string {\n return `${this.prefix}:{${scope}}`;\n }\n\n private async load(scope: string): Promise<{ state: ScopeState | null; version: number }> {\n const reply = (await this.client.eval(READ, 1, this.redisKey(scope))) as Array<string | null>;\n const raw = reply?.[0] ?? null;\n const version = reply?.[1] ? Number(reply[1]) : 0;\n return { state: raw ? (JSON.parse(raw) as ScopeState) : null, version };\n }\n\n async read(scope: string): Promise<ScopeState | null> {\n return (await this.load(scope)).state;\n }\n\n async transact<T>(scope: string, fn: (state: ScopeState | null) => TransactResult<T>): Promise<T> {\n const key = this.redisKey(scope);\n for (let attempt = 0; attempt <= this.maxRetries; attempt++) {\n const { state, version } = await this.load(scope);\n const { next, result } = fn(state);\n // A read-only decision was taken against one consistent snapshot, so it stands.\n if (!next) return result;\n const ok = await this.client.eval(CAS, 1, key, version, JSON.stringify(next), this.keyTtlMs);\n if (Number(ok) === 1) return result;\n const cap = Math.min(50, 2 ** Math.min(attempt, 6));\n await new Promise((r) => setTimeout(r, Math.random() * cap));\n }\n throw new StoreContentionError(scope, this.maxRetries + 1);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAoBA,IAAM,OAAO;AAEb,IAAM,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASL,IAAM,uBAAN,cAAmC,MAAM;AAAA,EAC9C,YAAY,OAAe,UAAkB;AAC3C,UAAM,+BAA+B,KAAK,UAAU,KAAK,CAAC,UAAU,QAAQ,qBAAqB;AACjG,SAAK,OAAO;AAAA,EACd;AACF;AAOO,IAAM,aAAN,MAAkC;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,SAA4B;AACtC,QAAI,CAAC,SAAS,OAAQ,OAAM,IAAI,UAAU,sCAAsC;AAChF,SAAK,SAAS,QAAQ;AACtB,SAAK,SAAS,QAAQ,UAAU;AAChC,SAAK,WAAW,QAAQ,YAAY,KAAK,KAAK;AAC9C,SAAK,aAAa,QAAQ,cAAc;AAAA,EAC1C;AAAA,EAEQ,SAAS,OAAuB;AACtC,WAAO,GAAG,KAAK,MAAM,KAAK,KAAK;AAAA,EACjC;AAAA,EAEA,MAAc,KAAK,OAAuE;AACxF,UAAM,QAAS,MAAM,KAAK,OAAO,KAAK,MAAM,GAAG,KAAK,SAAS,KAAK,CAAC;AACnE,UAAM,MAAM,QAAQ,CAAC,KAAK;AAC1B,UAAM,UAAU,QAAQ,CAAC,IAAI,OAAO,MAAM,CAAC,CAAC,IAAI;AAChD,WAAO,EAAE,OAAO,MAAO,KAAK,MAAM,GAAG,IAAmB,MAAM,QAAQ;AAAA,EACxE;AAAA,EAEA,MAAM,KAAK,OAA2C;AACpD,YAAQ,MAAM,KAAK,KAAK,KAAK,GAAG;AAAA,EAClC;AAAA,EAEA,MAAM,SAAY,OAAe,IAAiE;AAChG,UAAM,MAAM,KAAK,SAAS,KAAK;AAC/B,aAAS,UAAU,GAAG,WAAW,KAAK,YAAY,WAAW;AAC3D,YAAM,EAAE,OAAO,QAAQ,IAAI,MAAM,KAAK,KAAK,KAAK;AAChD,YAAM,EAAE,MAAM,OAAO,IAAI,GAAG,KAAK;AAEjC,UAAI,CAAC,KAAM,QAAO;AAClB,YAAM,KAAK,MAAM,KAAK,OAAO,KAAK,KAAK,GAAG,KAAK,SAAS,KAAK,UAAU,IAAI,GAAG,KAAK,QAAQ;AAC3F,UAAI,OAAO,EAAE,MAAM,EAAG,QAAO;AAC7B,YAAM,MAAM,KAAK,IAAI,IAAI,KAAK,KAAK,IAAI,SAAS,CAAC,CAAC;AAClD,YAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,KAAK,OAAO,IAAI,GAAG,CAAC;AAAA,IAC7D;AACA,UAAM,IAAI,qBAAqB,OAAO,KAAK,aAAa,CAAC;AAAA,EAC3D;AACF;","names":[]}
|
package/dist/redis.d.cts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Store, ScopeState, TransactResult } from './index.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The one Redis command replyonce uses, in ioredis' signature. For node-redis, pass
|
|
5
|
+
* `{ eval: (script, n, ...args) => client.eval(script, { keys: args.slice(0, n), arguments: args.slice(n) }) }`.
|
|
6
|
+
*/
|
|
7
|
+
interface RedisLike {
|
|
8
|
+
eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;
|
|
9
|
+
}
|
|
10
|
+
interface RedisStoreOptions {
|
|
11
|
+
client: RedisLike;
|
|
12
|
+
/** Key prefix. Default `replyonce`. */
|
|
13
|
+
prefix?: string;
|
|
14
|
+
/** Expire idle scopes after this long. 0 disables expiry. Default 30 days. */
|
|
15
|
+
keyTtlMs?: number;
|
|
16
|
+
/** Optimistic retries under contention before giving up. Default 200. */
|
|
17
|
+
maxRetries?: number;
|
|
18
|
+
}
|
|
19
|
+
declare class StoreContentionError extends Error {
|
|
20
|
+
constructor(scope: string, attempts: number);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Redis store. Each scope is one hash holding the state and a version. `transact` reads
|
|
24
|
+
* both, decides locally, then writes with a compare-and-set Lua script, retrying on a lost
|
|
25
|
+
* race. Works on a shared connection (no WATCH) and with Redis Cluster (one key per op).
|
|
26
|
+
*/
|
|
27
|
+
declare class RedisStore implements Store {
|
|
28
|
+
private readonly client;
|
|
29
|
+
private readonly prefix;
|
|
30
|
+
private readonly keyTtlMs;
|
|
31
|
+
private readonly maxRetries;
|
|
32
|
+
constructor(options: RedisStoreOptions);
|
|
33
|
+
private redisKey;
|
|
34
|
+
private load;
|
|
35
|
+
read(scope: string): Promise<ScopeState | null>;
|
|
36
|
+
transact<T>(scope: string, fn: (state: ScopeState | null) => TransactResult<T>): Promise<T>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { RedisStore, StoreContentionError };
|
|
40
|
+
export type { RedisLike, RedisStoreOptions };
|
package/dist/redis.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Store, ScopeState, TransactResult } from './index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The one Redis command replyonce uses, in ioredis' signature. For node-redis, pass
|
|
5
|
+
* `{ eval: (script, n, ...args) => client.eval(script, { keys: args.slice(0, n), arguments: args.slice(n) }) }`.
|
|
6
|
+
*/
|
|
7
|
+
interface RedisLike {
|
|
8
|
+
eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;
|
|
9
|
+
}
|
|
10
|
+
interface RedisStoreOptions {
|
|
11
|
+
client: RedisLike;
|
|
12
|
+
/** Key prefix. Default `replyonce`. */
|
|
13
|
+
prefix?: string;
|
|
14
|
+
/** Expire idle scopes after this long. 0 disables expiry. Default 30 days. */
|
|
15
|
+
keyTtlMs?: number;
|
|
16
|
+
/** Optimistic retries under contention before giving up. Default 200. */
|
|
17
|
+
maxRetries?: number;
|
|
18
|
+
}
|
|
19
|
+
declare class StoreContentionError extends Error {
|
|
20
|
+
constructor(scope: string, attempts: number);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Redis store. Each scope is one hash holding the state and a version. `transact` reads
|
|
24
|
+
* both, decides locally, then writes with a compare-and-set Lua script, retrying on a lost
|
|
25
|
+
* race. Works on a shared connection (no WATCH) and with Redis Cluster (one key per op).
|
|
26
|
+
*/
|
|
27
|
+
declare class RedisStore implements Store {
|
|
28
|
+
private readonly client;
|
|
29
|
+
private readonly prefix;
|
|
30
|
+
private readonly keyTtlMs;
|
|
31
|
+
private readonly maxRetries;
|
|
32
|
+
constructor(options: RedisStoreOptions);
|
|
33
|
+
private redisKey;
|
|
34
|
+
private load;
|
|
35
|
+
read(scope: string): Promise<ScopeState | null>;
|
|
36
|
+
transact<T>(scope: string, fn: (state: ScopeState | null) => TransactResult<T>): Promise<T>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { RedisStore, StoreContentionError };
|
|
40
|
+
export type { RedisLike, RedisStoreOptions };
|
package/dist/redis.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// src/stores/redis.ts
|
|
2
|
+
var READ = `return redis.call('HMGET', KEYS[1], 's', 'v')`;
|
|
3
|
+
var CAS = `
|
|
4
|
+
local v = tonumber(redis.call('HGET', KEYS[1], 'v') or '0')
|
|
5
|
+
if v ~= tonumber(ARGV[1]) then return 0 end
|
|
6
|
+
redis.call('HSET', KEYS[1], 's', ARGV[2], 'v', v + 1)
|
|
7
|
+
local ttl = tonumber(ARGV[3])
|
|
8
|
+
if ttl > 0 then redis.call('PEXPIRE', KEYS[1], ttl) end
|
|
9
|
+
return 1
|
|
10
|
+
`;
|
|
11
|
+
var StoreContentionError = class extends Error {
|
|
12
|
+
constructor(scope, attempts) {
|
|
13
|
+
super(`replyonce: gave up on scope ${JSON.stringify(scope)} after ${attempts} contended attempts`);
|
|
14
|
+
this.name = "StoreContentionError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var RedisStore = class {
|
|
18
|
+
client;
|
|
19
|
+
prefix;
|
|
20
|
+
keyTtlMs;
|
|
21
|
+
maxRetries;
|
|
22
|
+
constructor(options) {
|
|
23
|
+
if (!options?.client) throw new TypeError("replyonce: RedisStore needs a client");
|
|
24
|
+
this.client = options.client;
|
|
25
|
+
this.prefix = options.prefix ?? "replyonce";
|
|
26
|
+
this.keyTtlMs = options.keyTtlMs ?? 30 * 24 * 36e5;
|
|
27
|
+
this.maxRetries = options.maxRetries ?? 200;
|
|
28
|
+
}
|
|
29
|
+
redisKey(scope) {
|
|
30
|
+
return `${this.prefix}:{${scope}}`;
|
|
31
|
+
}
|
|
32
|
+
async load(scope) {
|
|
33
|
+
const reply = await this.client.eval(READ, 1, this.redisKey(scope));
|
|
34
|
+
const raw = reply?.[0] ?? null;
|
|
35
|
+
const version = reply?.[1] ? Number(reply[1]) : 0;
|
|
36
|
+
return { state: raw ? JSON.parse(raw) : null, version };
|
|
37
|
+
}
|
|
38
|
+
async read(scope) {
|
|
39
|
+
return (await this.load(scope)).state;
|
|
40
|
+
}
|
|
41
|
+
async transact(scope, fn) {
|
|
42
|
+
const key = this.redisKey(scope);
|
|
43
|
+
for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
|
|
44
|
+
const { state, version } = await this.load(scope);
|
|
45
|
+
const { next, result } = fn(state);
|
|
46
|
+
if (!next) return result;
|
|
47
|
+
const ok = await this.client.eval(CAS, 1, key, version, JSON.stringify(next), this.keyTtlMs);
|
|
48
|
+
if (Number(ok) === 1) return result;
|
|
49
|
+
const cap = Math.min(50, 2 ** Math.min(attempt, 6));
|
|
50
|
+
await new Promise((r) => setTimeout(r, Math.random() * cap));
|
|
51
|
+
}
|
|
52
|
+
throw new StoreContentionError(scope, this.maxRetries + 1);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
export {
|
|
56
|
+
RedisStore,
|
|
57
|
+
StoreContentionError
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=redis.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/stores/redis.ts"],"sourcesContent":["import type { ScopeState, Store, TransactResult } from '../types.js';\n\n/**\n * The one Redis command replyonce uses, in ioredis' signature. For node-redis, pass\n * `{ eval: (script, n, ...args) => client.eval(script, { keys: args.slice(0, n), arguments: args.slice(n) }) }`.\n */\nexport interface RedisLike {\n eval(script: string, numKeys: number, ...args: Array<string | number>): Promise<unknown>;\n}\n\nexport interface RedisStoreOptions {\n client: RedisLike;\n /** Key prefix. Default `replyonce`. */\n prefix?: string;\n /** Expire idle scopes after this long. 0 disables expiry. Default 30 days. */\n keyTtlMs?: number;\n /** Optimistic retries under contention before giving up. Default 200. */\n maxRetries?: number;\n}\n\nconst READ = `return redis.call('HMGET', KEYS[1], 's', 'v')`;\n\nconst CAS = `\nlocal v = tonumber(redis.call('HGET', KEYS[1], 'v') or '0')\nif v ~= tonumber(ARGV[1]) then return 0 end\nredis.call('HSET', KEYS[1], 's', ARGV[2], 'v', v + 1)\nlocal ttl = tonumber(ARGV[3])\nif ttl > 0 then redis.call('PEXPIRE', KEYS[1], ttl) end\nreturn 1\n`;\n\nexport class StoreContentionError extends Error {\n constructor(scope: string, attempts: number) {\n super(`replyonce: gave up on scope ${JSON.stringify(scope)} after ${attempts} contended attempts`);\n this.name = 'StoreContentionError';\n }\n}\n\n/**\n * Redis store. Each scope is one hash holding the state and a version. `transact` reads\n * both, decides locally, then writes with a compare-and-set Lua script, retrying on a lost\n * race. Works on a shared connection (no WATCH) and with Redis Cluster (one key per op).\n */\nexport class RedisStore implements Store {\n private readonly client: RedisLike;\n private readonly prefix: string;\n private readonly keyTtlMs: number;\n private readonly maxRetries: number;\n\n constructor(options: RedisStoreOptions) {\n if (!options?.client) throw new TypeError('replyonce: RedisStore needs a client');\n this.client = options.client;\n this.prefix = options.prefix ?? 'replyonce';\n this.keyTtlMs = options.keyTtlMs ?? 30 * 24 * 3_600_000;\n this.maxRetries = options.maxRetries ?? 200;\n }\n\n private redisKey(scope: string): string {\n return `${this.prefix}:{${scope}}`;\n }\n\n private async load(scope: string): Promise<{ state: ScopeState | null; version: number }> {\n const reply = (await this.client.eval(READ, 1, this.redisKey(scope))) as Array<string | null>;\n const raw = reply?.[0] ?? null;\n const version = reply?.[1] ? Number(reply[1]) : 0;\n return { state: raw ? (JSON.parse(raw) as ScopeState) : null, version };\n }\n\n async read(scope: string): Promise<ScopeState | null> {\n return (await this.load(scope)).state;\n }\n\n async transact<T>(scope: string, fn: (state: ScopeState | null) => TransactResult<T>): Promise<T> {\n const key = this.redisKey(scope);\n for (let attempt = 0; attempt <= this.maxRetries; attempt++) {\n const { state, version } = await this.load(scope);\n const { next, result } = fn(state);\n // A read-only decision was taken against one consistent snapshot, so it stands.\n if (!next) return result;\n const ok = await this.client.eval(CAS, 1, key, version, JSON.stringify(next), this.keyTtlMs);\n if (Number(ok) === 1) return result;\n const cap = Math.min(50, 2 ** Math.min(attempt, 6));\n await new Promise((r) => setTimeout(r, Math.random() * cap));\n }\n throw new StoreContentionError(scope, this.maxRetries + 1);\n }\n}\n"],"mappings":";AAoBA,IAAM,OAAO;AAEb,IAAM,MAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AASL,IAAM,uBAAN,cAAmC,MAAM;AAAA,EAC9C,YAAY,OAAe,UAAkB;AAC3C,UAAM,+BAA+B,KAAK,UAAU,KAAK,CAAC,UAAU,QAAQ,qBAAqB;AACjG,SAAK,OAAO;AAAA,EACd;AACF;AAOO,IAAM,aAAN,MAAkC;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAEjB,YAAY,SAA4B;AACtC,QAAI,CAAC,SAAS,OAAQ,OAAM,IAAI,UAAU,sCAAsC;AAChF,SAAK,SAAS,QAAQ;AACtB,SAAK,SAAS,QAAQ,UAAU;AAChC,SAAK,WAAW,QAAQ,YAAY,KAAK,KAAK;AAC9C,SAAK,aAAa,QAAQ,cAAc;AAAA,EAC1C;AAAA,EAEQ,SAAS,OAAuB;AACtC,WAAO,GAAG,KAAK,MAAM,KAAK,KAAK;AAAA,EACjC;AAAA,EAEA,MAAc,KAAK,OAAuE;AACxF,UAAM,QAAS,MAAM,KAAK,OAAO,KAAK,MAAM,GAAG,KAAK,SAAS,KAAK,CAAC;AACnE,UAAM,MAAM,QAAQ,CAAC,KAAK;AAC1B,UAAM,UAAU,QAAQ,CAAC,IAAI,OAAO,MAAM,CAAC,CAAC,IAAI;AAChD,WAAO,EAAE,OAAO,MAAO,KAAK,MAAM,GAAG,IAAmB,MAAM,QAAQ;AAAA,EACxE;AAAA,EAEA,MAAM,KAAK,OAA2C;AACpD,YAAQ,MAAM,KAAK,KAAK,KAAK,GAAG;AAAA,EAClC;AAAA,EAEA,MAAM,SAAY,OAAe,IAAiE;AAChG,UAAM,MAAM,KAAK,SAAS,KAAK;AAC/B,aAAS,UAAU,GAAG,WAAW,KAAK,YAAY,WAAW;AAC3D,YAAM,EAAE,OAAO,QAAQ,IAAI,MAAM,KAAK,KAAK,KAAK;AAChD,YAAM,EAAE,MAAM,OAAO,IAAI,GAAG,KAAK;AAEjC,UAAI,CAAC,KAAM,QAAO;AAClB,YAAM,KAAK,MAAM,KAAK,OAAO,KAAK,KAAK,GAAG,KAAK,SAAS,KAAK,UAAU,IAAI,GAAG,KAAK,QAAQ;AAC3F,UAAI,OAAO,EAAE,MAAM,EAAG,QAAO;AAC7B,YAAM,MAAM,KAAK,IAAI,IAAI,KAAK,KAAK,IAAI,SAAS,CAAC,CAAC;AAClD,YAAM,IAAI,QAAQ,CAAC,MAAM,WAAW,GAAG,KAAK,OAAO,IAAI,GAAG,CAAC;AAAA,IAC7D;AACA,UAAM,IAAI,qBAAqB,OAAO,KAAK,aAAa,CAAC;AAAA,EAC3D;AACF;","names":[]}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { i as RunRecord, b as EffectRecord, S as Store, d as ErrorClass, e as ReconcileVerdict, g as ReplyOnceEvent, E as EffectContext, O as OnceResult, c as EffectStatus, J as Json, R as RunMode, h as RunEndReason } from './types-Dn7TSEnu.cjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The replyonce state machine. Every function here is pure: it takes a scope's state and
|
|
5
|
+
* returns the next state plus a decision. Stores only make each call atomic.
|
|
6
|
+
*/
|
|
7
|
+
interface Policy {
|
|
8
|
+
pendingTtlMs: number;
|
|
9
|
+
maxAttempts: number;
|
|
10
|
+
retentionMs: number;
|
|
11
|
+
}
|
|
12
|
+
type ConflictMode = 'any' | 'same-intent' | 'none';
|
|
13
|
+
type BeginOutcome = {
|
|
14
|
+
state: 'new' | 'resumed' | 'superseded' | 'ended';
|
|
15
|
+
run: RunRecord;
|
|
16
|
+
priorEffects: EffectRecord[];
|
|
17
|
+
} | {
|
|
18
|
+
state: 'busy';
|
|
19
|
+
holder: {
|
|
20
|
+
runId: string;
|
|
21
|
+
token: number;
|
|
22
|
+
leaseExpiresAt: number;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Throw this (or anything with `replyonce: 'not_applied'`) when the effect definitely did not happen. */
|
|
27
|
+
declare class NotApplied extends Error {
|
|
28
|
+
readonly replyonce: "not_applied";
|
|
29
|
+
constructor(message?: string, options?: {
|
|
30
|
+
cause?: unknown;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
declare function defaultClassifyError(err: unknown): ErrorClass;
|
|
34
|
+
interface KeyParts {
|
|
35
|
+
/** Usually the conversation or thread id. All fencing happens within a scope. */
|
|
36
|
+
scope: string;
|
|
37
|
+
/** What the effect is: `reply`, `refund`, `create_ticket`. */
|
|
38
|
+
intent: string;
|
|
39
|
+
/**
|
|
40
|
+
* The business identity of the effect, hashed into the key. Use what makes it the same
|
|
41
|
+
* action (the inbound message being answered, the order being refunded), never
|
|
42
|
+
* model-generated text or a model tool-call id.
|
|
43
|
+
*/
|
|
44
|
+
payload?: unknown;
|
|
45
|
+
}
|
|
46
|
+
type Reconciler = (entry: EffectRecord) => Promise<ReconcileVerdict>;
|
|
47
|
+
interface ReplyOnceOptions {
|
|
48
|
+
store: Store;
|
|
49
|
+
/** How long a claim may stay pending before it is treated as abandoned (ambiguous). Default 15 min. */
|
|
50
|
+
pendingTtlMs?: number;
|
|
51
|
+
/** Attempts per key, counting only attempts known not to have applied. Default 3. */
|
|
52
|
+
maxAttempts?: number;
|
|
53
|
+
/** How long settled effects and ended runs are remembered. Default 7 days. */
|
|
54
|
+
retentionMs?: number;
|
|
55
|
+
/** Run lease length; renewed on every claim and heartbeat. Default 60 s. */
|
|
56
|
+
leaseTtlMs?: number;
|
|
57
|
+
/** Decide whether a thrown error means "did not happen" or "might have happened". */
|
|
58
|
+
classifyError?: (err: unknown) => ErrorClass;
|
|
59
|
+
/** Ask the outside world what happened to an ambiguous effect. */
|
|
60
|
+
reconcile?: Reconciler;
|
|
61
|
+
onEvent?: (event: ReplyOnceEvent) => void;
|
|
62
|
+
clock?: () => number;
|
|
63
|
+
}
|
|
64
|
+
interface OnceOptions {
|
|
65
|
+
/** Short human-readable description, shown to other runs in conflict reports. */
|
|
66
|
+
summary?: string;
|
|
67
|
+
classifyError?: (err: unknown) => ErrorClass;
|
|
68
|
+
reconcile?: Reconciler;
|
|
69
|
+
/** Wait for an in-flight duplicate to settle instead of returning `in_flight` immediately. */
|
|
70
|
+
wait?: {
|
|
71
|
+
timeoutMs: number;
|
|
72
|
+
intervalMs?: number;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
interface RunOnceOptions extends OnceOptions {
|
|
76
|
+
/**
|
|
77
|
+
* `any` (default): refuse if another run wrote anything on this scope since this run last
|
|
78
|
+
* looked. `same-intent`: only effects with the same intent conflict. `none`: per-key
|
|
79
|
+
* idempotency and supersession only.
|
|
80
|
+
*/
|
|
81
|
+
conflict?: ConflictMode;
|
|
82
|
+
/** Record that this run has now seen the conflicts, so its next attempt can proceed. Default true. */
|
|
83
|
+
ack?: boolean;
|
|
84
|
+
}
|
|
85
|
+
interface CheckResult {
|
|
86
|
+
/** `true` only when the effect is known to have happened. */
|
|
87
|
+
done: boolean;
|
|
88
|
+
status: EffectStatus | 'none';
|
|
89
|
+
entry: EffectRecord | null;
|
|
90
|
+
}
|
|
91
|
+
interface BeginOptions {
|
|
92
|
+
/** Stable id for this unit of work. Reuse it on redelivery (for example the queue job id). */
|
|
93
|
+
runId?: string;
|
|
94
|
+
mode?: RunMode;
|
|
95
|
+
leaseTtlMs?: number;
|
|
96
|
+
/**
|
|
97
|
+
* Epoch ms when this run's context (the conversation it will act on) was read, for
|
|
98
|
+
* example when the webhook payload was captured. Writes by other runs that landed
|
|
99
|
+
* after this point are reported as `conversation_changed`. Default: now.
|
|
100
|
+
*/
|
|
101
|
+
contextAt?: number;
|
|
102
|
+
}
|
|
103
|
+
type Effect<T> = (ctx: EffectContext) => Promise<T> | T;
|
|
104
|
+
declare class ReplyOnce {
|
|
105
|
+
readonly store: Store;
|
|
106
|
+
readonly policy: Policy;
|
|
107
|
+
readonly leaseTtlMs: number;
|
|
108
|
+
private readonly classify;
|
|
109
|
+
private readonly reconciler;
|
|
110
|
+
private readonly emit;
|
|
111
|
+
private readonly now;
|
|
112
|
+
constructor(options: ReplyOnceOptions);
|
|
113
|
+
/** Resolve key parts to the ledger key used for them. */
|
|
114
|
+
key(parts: KeyParts): string;
|
|
115
|
+
/**
|
|
116
|
+
* Run `effect` at most once for this key. A string key is its own scope; key parts give
|
|
117
|
+
* a scope (conversation) plus an intent and business identity.
|
|
118
|
+
*/
|
|
119
|
+
once<T>(key: string | KeyParts, effect: Effect<T>, options?: OnceOptions): Promise<OnceResult<T>>;
|
|
120
|
+
/** "Was this already done?" — safe to expose to a model as a read-only tool. */
|
|
121
|
+
check(key: string | KeyParts): Promise<CheckResult>;
|
|
122
|
+
/** Every effect recorded on a scope, oldest first. */
|
|
123
|
+
history(scope: string): Promise<EffectRecord[]>;
|
|
124
|
+
/** Record what an operator or reconciler found out about an ambiguous effect. */
|
|
125
|
+
resolve(key: string | KeyParts, verdict: {
|
|
126
|
+
outcome: 'committed';
|
|
127
|
+
result?: Json;
|
|
128
|
+
} | {
|
|
129
|
+
outcome: 'not_applied';
|
|
130
|
+
}): Promise<EffectRecord | null>;
|
|
131
|
+
/**
|
|
132
|
+
* Start (or, with the same `runId`, resume) a unit of work on a scope. The run gets a
|
|
133
|
+
* fencing token; its writes are refused once a newer run owns the scope.
|
|
134
|
+
*/
|
|
135
|
+
begin(scope: string, options?: BeginOptions): Promise<Run>;
|
|
136
|
+
/** Cancel a run: its future writes are refused with `run_cancelled`. */
|
|
137
|
+
cancel(scope: string, runId: string): Promise<boolean>;
|
|
138
|
+
/** @internal */
|
|
139
|
+
renewRun(scope: string, runId: string, token: number, leaseTtlMs: number): Promise<boolean>;
|
|
140
|
+
/** @internal */
|
|
141
|
+
endRun(scope: string, runId: string, token: number, reason: RunEndReason): Promise<void>;
|
|
142
|
+
/** @internal */
|
|
143
|
+
resolveKey(key: string | KeyParts): {
|
|
144
|
+
scope: string;
|
|
145
|
+
ledgerKey: string;
|
|
146
|
+
intent: string;
|
|
147
|
+
};
|
|
148
|
+
/** @internal */
|
|
149
|
+
execute<T>(scope: string, key: string, intent: string, run: {
|
|
150
|
+
runId: string;
|
|
151
|
+
token: number;
|
|
152
|
+
leaseTtlMs: number;
|
|
153
|
+
} | null, effect: Effect<T>, options: RunOnceOptions): Promise<OnceResult<T>>;
|
|
154
|
+
private runAcquired;
|
|
155
|
+
private settleWithRetry;
|
|
156
|
+
private waitForSettle;
|
|
157
|
+
}
|
|
158
|
+
type RunState = BeginOutcome['state'];
|
|
159
|
+
/** A unit of agent work on one scope, holding a fencing token. */
|
|
160
|
+
declare class Run {
|
|
161
|
+
readonly scope: string;
|
|
162
|
+
readonly runId: string;
|
|
163
|
+
/** `new`, `resumed` (same runId seen before), `superseded`, `ended`, or `busy` (exclusive mode). */
|
|
164
|
+
readonly state: RunState;
|
|
165
|
+
/** Monotonic per scope. `null` when the run could not start (`busy`). */
|
|
166
|
+
readonly fencingToken: number | null;
|
|
167
|
+
/** Effects this runId already claimed before (non-empty on redelivery). */
|
|
168
|
+
readonly priorEffects: EffectRecord[];
|
|
169
|
+
readonly busyHolder: {
|
|
170
|
+
runId: string;
|
|
171
|
+
token: number;
|
|
172
|
+
leaseExpiresAt: number;
|
|
173
|
+
} | null;
|
|
174
|
+
private readonly ro;
|
|
175
|
+
private readonly leaseTtlMs;
|
|
176
|
+
constructor(ro: ReplyOnce, scope: string, runId: string, leaseTtlMs: number, outcome: BeginOutcome);
|
|
177
|
+
/** `true` when this run may write: it is new or resumed, not superseded, ended or busy. */
|
|
178
|
+
get canWrite(): boolean;
|
|
179
|
+
/**
|
|
180
|
+
* `true` when a previous delivery of this run already claimed a write that may have
|
|
181
|
+
* reached the outside world (pending, committed or ambiguous).
|
|
182
|
+
*/
|
|
183
|
+
get alreadyWrote(): boolean;
|
|
184
|
+
/** Run `effect` at most once per (scope, intent, payload), fenced to this run. */
|
|
185
|
+
once<T>(intent: string, payload: unknown, effect: Effect<T>, options?: RunOnceOptions): Promise<OnceResult<T>>;
|
|
186
|
+
check(intent: string, payload?: unknown): Promise<CheckResult>;
|
|
187
|
+
history(): Promise<EffectRecord[]>;
|
|
188
|
+
/**
|
|
189
|
+
* Renew the lease. `false` means stop: the run was superseded (by a newer run or a later
|
|
190
|
+
* delivery of this one), cancelled or ended.
|
|
191
|
+
*/
|
|
192
|
+
heartbeat(): Promise<boolean>;
|
|
193
|
+
/** Mark the run finished. A no-op if a later delivery of the same run has taken over. */
|
|
194
|
+
end(reason?: RunEndReason): Promise<void>;
|
|
195
|
+
}
|
|
196
|
+
declare function createReplyOnce(options: ReplyOnceOptions): ReplyOnce;
|
|
197
|
+
/** Return the value of a successful result, or throw a `ReplyOnceError` describing why not. */
|
|
198
|
+
declare function unwrap<T>(result: OnceResult<T>): T;
|
|
199
|
+
declare class ReplyOnceError extends Error {
|
|
200
|
+
readonly result: Exclude<OnceResult<unknown>, {
|
|
201
|
+
ok: true;
|
|
202
|
+
}>;
|
|
203
|
+
constructor(result: Exclude<OnceResult<unknown>, {
|
|
204
|
+
ok: true;
|
|
205
|
+
}>);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export { NotApplied as N, Run as R, ReplyOnce as a, ReplyOnceError as d, createReplyOnce as h, defaultClassifyError as i, unwrap as u };
|
|
209
|
+
export type { BeginOptions as B, ConflictMode as C, Effect as E, KeyParts as K, OnceOptions as O, CheckResult as b, Reconciler as c, ReplyOnceOptions as e, RunOnceOptions as f, RunState as g };
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import { i as RunRecord, b as EffectRecord, S as Store, d as ErrorClass, e as ReconcileVerdict, g as ReplyOnceEvent, E as EffectContext, O as OnceResult, c as EffectStatus, J as Json, R as RunMode, h as RunEndReason } from './types-Dn7TSEnu.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The replyonce state machine. Every function here is pure: it takes a scope's state and
|
|
5
|
+
* returns the next state plus a decision. Stores only make each call atomic.
|
|
6
|
+
*/
|
|
7
|
+
interface Policy {
|
|
8
|
+
pendingTtlMs: number;
|
|
9
|
+
maxAttempts: number;
|
|
10
|
+
retentionMs: number;
|
|
11
|
+
}
|
|
12
|
+
type ConflictMode = 'any' | 'same-intent' | 'none';
|
|
13
|
+
type BeginOutcome = {
|
|
14
|
+
state: 'new' | 'resumed' | 'superseded' | 'ended';
|
|
15
|
+
run: RunRecord;
|
|
16
|
+
priorEffects: EffectRecord[];
|
|
17
|
+
} | {
|
|
18
|
+
state: 'busy';
|
|
19
|
+
holder: {
|
|
20
|
+
runId: string;
|
|
21
|
+
token: number;
|
|
22
|
+
leaseExpiresAt: number;
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/** Throw this (or anything with `replyonce: 'not_applied'`) when the effect definitely did not happen. */
|
|
27
|
+
declare class NotApplied extends Error {
|
|
28
|
+
readonly replyonce: "not_applied";
|
|
29
|
+
constructor(message?: string, options?: {
|
|
30
|
+
cause?: unknown;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
declare function defaultClassifyError(err: unknown): ErrorClass;
|
|
34
|
+
interface KeyParts {
|
|
35
|
+
/** Usually the conversation or thread id. All fencing happens within a scope. */
|
|
36
|
+
scope: string;
|
|
37
|
+
/** What the effect is: `reply`, `refund`, `create_ticket`. */
|
|
38
|
+
intent: string;
|
|
39
|
+
/**
|
|
40
|
+
* The business identity of the effect, hashed into the key. Use what makes it the same
|
|
41
|
+
* action (the inbound message being answered, the order being refunded), never
|
|
42
|
+
* model-generated text or a model tool-call id.
|
|
43
|
+
*/
|
|
44
|
+
payload?: unknown;
|
|
45
|
+
}
|
|
46
|
+
type Reconciler = (entry: EffectRecord) => Promise<ReconcileVerdict>;
|
|
47
|
+
interface ReplyOnceOptions {
|
|
48
|
+
store: Store;
|
|
49
|
+
/** How long a claim may stay pending before it is treated as abandoned (ambiguous). Default 15 min. */
|
|
50
|
+
pendingTtlMs?: number;
|
|
51
|
+
/** Attempts per key, counting only attempts known not to have applied. Default 3. */
|
|
52
|
+
maxAttempts?: number;
|
|
53
|
+
/** How long settled effects and ended runs are remembered. Default 7 days. */
|
|
54
|
+
retentionMs?: number;
|
|
55
|
+
/** Run lease length; renewed on every claim and heartbeat. Default 60 s. */
|
|
56
|
+
leaseTtlMs?: number;
|
|
57
|
+
/** Decide whether a thrown error means "did not happen" or "might have happened". */
|
|
58
|
+
classifyError?: (err: unknown) => ErrorClass;
|
|
59
|
+
/** Ask the outside world what happened to an ambiguous effect. */
|
|
60
|
+
reconcile?: Reconciler;
|
|
61
|
+
onEvent?: (event: ReplyOnceEvent) => void;
|
|
62
|
+
clock?: () => number;
|
|
63
|
+
}
|
|
64
|
+
interface OnceOptions {
|
|
65
|
+
/** Short human-readable description, shown to other runs in conflict reports. */
|
|
66
|
+
summary?: string;
|
|
67
|
+
classifyError?: (err: unknown) => ErrorClass;
|
|
68
|
+
reconcile?: Reconciler;
|
|
69
|
+
/** Wait for an in-flight duplicate to settle instead of returning `in_flight` immediately. */
|
|
70
|
+
wait?: {
|
|
71
|
+
timeoutMs: number;
|
|
72
|
+
intervalMs?: number;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
interface RunOnceOptions extends OnceOptions {
|
|
76
|
+
/**
|
|
77
|
+
* `any` (default): refuse if another run wrote anything on this scope since this run last
|
|
78
|
+
* looked. `same-intent`: only effects with the same intent conflict. `none`: per-key
|
|
79
|
+
* idempotency and supersession only.
|
|
80
|
+
*/
|
|
81
|
+
conflict?: ConflictMode;
|
|
82
|
+
/** Record that this run has now seen the conflicts, so its next attempt can proceed. Default true. */
|
|
83
|
+
ack?: boolean;
|
|
84
|
+
}
|
|
85
|
+
interface CheckResult {
|
|
86
|
+
/** `true` only when the effect is known to have happened. */
|
|
87
|
+
done: boolean;
|
|
88
|
+
status: EffectStatus | 'none';
|
|
89
|
+
entry: EffectRecord | null;
|
|
90
|
+
}
|
|
91
|
+
interface BeginOptions {
|
|
92
|
+
/** Stable id for this unit of work. Reuse it on redelivery (for example the queue job id). */
|
|
93
|
+
runId?: string;
|
|
94
|
+
mode?: RunMode;
|
|
95
|
+
leaseTtlMs?: number;
|
|
96
|
+
/**
|
|
97
|
+
* Epoch ms when this run's context (the conversation it will act on) was read, for
|
|
98
|
+
* example when the webhook payload was captured. Writes by other runs that landed
|
|
99
|
+
* after this point are reported as `conversation_changed`. Default: now.
|
|
100
|
+
*/
|
|
101
|
+
contextAt?: number;
|
|
102
|
+
}
|
|
103
|
+
type Effect<T> = (ctx: EffectContext) => Promise<T> | T;
|
|
104
|
+
declare class ReplyOnce {
|
|
105
|
+
readonly store: Store;
|
|
106
|
+
readonly policy: Policy;
|
|
107
|
+
readonly leaseTtlMs: number;
|
|
108
|
+
private readonly classify;
|
|
109
|
+
private readonly reconciler;
|
|
110
|
+
private readonly emit;
|
|
111
|
+
private readonly now;
|
|
112
|
+
constructor(options: ReplyOnceOptions);
|
|
113
|
+
/** Resolve key parts to the ledger key used for them. */
|
|
114
|
+
key(parts: KeyParts): string;
|
|
115
|
+
/**
|
|
116
|
+
* Run `effect` at most once for this key. A string key is its own scope; key parts give
|
|
117
|
+
* a scope (conversation) plus an intent and business identity.
|
|
118
|
+
*/
|
|
119
|
+
once<T>(key: string | KeyParts, effect: Effect<T>, options?: OnceOptions): Promise<OnceResult<T>>;
|
|
120
|
+
/** "Was this already done?" — safe to expose to a model as a read-only tool. */
|
|
121
|
+
check(key: string | KeyParts): Promise<CheckResult>;
|
|
122
|
+
/** Every effect recorded on a scope, oldest first. */
|
|
123
|
+
history(scope: string): Promise<EffectRecord[]>;
|
|
124
|
+
/** Record what an operator or reconciler found out about an ambiguous effect. */
|
|
125
|
+
resolve(key: string | KeyParts, verdict: {
|
|
126
|
+
outcome: 'committed';
|
|
127
|
+
result?: Json;
|
|
128
|
+
} | {
|
|
129
|
+
outcome: 'not_applied';
|
|
130
|
+
}): Promise<EffectRecord | null>;
|
|
131
|
+
/**
|
|
132
|
+
* Start (or, with the same `runId`, resume) a unit of work on a scope. The run gets a
|
|
133
|
+
* fencing token; its writes are refused once a newer run owns the scope.
|
|
134
|
+
*/
|
|
135
|
+
begin(scope: string, options?: BeginOptions): Promise<Run>;
|
|
136
|
+
/** Cancel a run: its future writes are refused with `run_cancelled`. */
|
|
137
|
+
cancel(scope: string, runId: string): Promise<boolean>;
|
|
138
|
+
/** @internal */
|
|
139
|
+
renewRun(scope: string, runId: string, token: number, leaseTtlMs: number): Promise<boolean>;
|
|
140
|
+
/** @internal */
|
|
141
|
+
endRun(scope: string, runId: string, token: number, reason: RunEndReason): Promise<void>;
|
|
142
|
+
/** @internal */
|
|
143
|
+
resolveKey(key: string | KeyParts): {
|
|
144
|
+
scope: string;
|
|
145
|
+
ledgerKey: string;
|
|
146
|
+
intent: string;
|
|
147
|
+
};
|
|
148
|
+
/** @internal */
|
|
149
|
+
execute<T>(scope: string, key: string, intent: string, run: {
|
|
150
|
+
runId: string;
|
|
151
|
+
token: number;
|
|
152
|
+
leaseTtlMs: number;
|
|
153
|
+
} | null, effect: Effect<T>, options: RunOnceOptions): Promise<OnceResult<T>>;
|
|
154
|
+
private runAcquired;
|
|
155
|
+
private settleWithRetry;
|
|
156
|
+
private waitForSettle;
|
|
157
|
+
}
|
|
158
|
+
type RunState = BeginOutcome['state'];
|
|
159
|
+
/** A unit of agent work on one scope, holding a fencing token. */
|
|
160
|
+
declare class Run {
|
|
161
|
+
readonly scope: string;
|
|
162
|
+
readonly runId: string;
|
|
163
|
+
/** `new`, `resumed` (same runId seen before), `superseded`, `ended`, or `busy` (exclusive mode). */
|
|
164
|
+
readonly state: RunState;
|
|
165
|
+
/** Monotonic per scope. `null` when the run could not start (`busy`). */
|
|
166
|
+
readonly fencingToken: number | null;
|
|
167
|
+
/** Effects this runId already claimed before (non-empty on redelivery). */
|
|
168
|
+
readonly priorEffects: EffectRecord[];
|
|
169
|
+
readonly busyHolder: {
|
|
170
|
+
runId: string;
|
|
171
|
+
token: number;
|
|
172
|
+
leaseExpiresAt: number;
|
|
173
|
+
} | null;
|
|
174
|
+
private readonly ro;
|
|
175
|
+
private readonly leaseTtlMs;
|
|
176
|
+
constructor(ro: ReplyOnce, scope: string, runId: string, leaseTtlMs: number, outcome: BeginOutcome);
|
|
177
|
+
/** `true` when this run may write: it is new or resumed, not superseded, ended or busy. */
|
|
178
|
+
get canWrite(): boolean;
|
|
179
|
+
/**
|
|
180
|
+
* `true` when a previous delivery of this run already claimed a write that may have
|
|
181
|
+
* reached the outside world (pending, committed or ambiguous).
|
|
182
|
+
*/
|
|
183
|
+
get alreadyWrote(): boolean;
|
|
184
|
+
/** Run `effect` at most once per (scope, intent, payload), fenced to this run. */
|
|
185
|
+
once<T>(intent: string, payload: unknown, effect: Effect<T>, options?: RunOnceOptions): Promise<OnceResult<T>>;
|
|
186
|
+
check(intent: string, payload?: unknown): Promise<CheckResult>;
|
|
187
|
+
history(): Promise<EffectRecord[]>;
|
|
188
|
+
/**
|
|
189
|
+
* Renew the lease. `false` means stop: the run was superseded (by a newer run or a later
|
|
190
|
+
* delivery of this one), cancelled or ended.
|
|
191
|
+
*/
|
|
192
|
+
heartbeat(): Promise<boolean>;
|
|
193
|
+
/** Mark the run finished. A no-op if a later delivery of the same run has taken over. */
|
|
194
|
+
end(reason?: RunEndReason): Promise<void>;
|
|
195
|
+
}
|
|
196
|
+
declare function createReplyOnce(options: ReplyOnceOptions): ReplyOnce;
|
|
197
|
+
/** Return the value of a successful result, or throw a `ReplyOnceError` describing why not. */
|
|
198
|
+
declare function unwrap<T>(result: OnceResult<T>): T;
|
|
199
|
+
declare class ReplyOnceError extends Error {
|
|
200
|
+
readonly result: Exclude<OnceResult<unknown>, {
|
|
201
|
+
ok: true;
|
|
202
|
+
}>;
|
|
203
|
+
constructor(result: Exclude<OnceResult<unknown>, {
|
|
204
|
+
ok: true;
|
|
205
|
+
}>);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export { NotApplied as N, Run as R, ReplyOnce as a, ReplyOnceError as d, createReplyOnce as h, defaultClassifyError as i, unwrap as u };
|
|
209
|
+
export type { BeginOptions as B, ConflictMode as C, Effect as E, KeyParts as K, OnceOptions as O, CheckResult as b, Reconciler as c, ReplyOnceOptions as e, RunOnceOptions as f, RunState as g };
|