@resonatehq/sdk 0.11.2 → 0.11.4
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/README.md +1 -1
- package/dist/{async → src/async}/context.d.ts +0 -1
- package/dist/{async → src/async}/context.js +0 -1
- package/dist/{async → src/async}/core.d.ts +0 -1
- package/dist/{async → src/async}/core.js +0 -1
- package/dist/{async → src/async}/index.d.ts +0 -1
- package/dist/{async → src/async}/index.js +0 -1
- package/dist/{async → src/async}/resonate.d.ts +0 -1
- package/dist/{async → src/async}/resonate.js +0 -1
- package/dist/{clock.d.ts → src/clock.d.ts} +0 -1
- package/dist/{clock.js → src/clock.js} +0 -1
- package/dist/{codec.d.ts → src/codec.d.ts} +0 -1
- package/dist/{codec.js → src/codec.js} +0 -1
- package/dist/{computation.d.ts → src/computation.d.ts} +0 -1
- package/dist/{computation.js → src/computation.js} +0 -1
- package/dist/{context.d.ts → src/context.d.ts} +0 -1
- package/dist/{context.js → src/context.js} +0 -1
- package/dist/{core.d.ts → src/core.d.ts} +0 -1
- package/dist/{core.js → src/core.js} +0 -1
- package/dist/{coroutine.d.ts → src/coroutine.d.ts} +0 -1
- package/dist/{coroutine.js → src/coroutine.js} +0 -1
- package/dist/{decorator.d.ts → src/decorator.d.ts} +0 -1
- package/dist/{decorator.js → src/decorator.js} +0 -1
- package/dist/{encryptor.d.ts → src/encryptor.d.ts} +0 -1
- package/dist/{encryptor.js → src/encryptor.js} +0 -1
- package/dist/{exceptions.d.ts → src/exceptions.d.ts} +0 -1
- package/dist/{exceptions.js → src/exceptions.js} +0 -1
- package/dist/{heartbeat.d.ts → src/heartbeat.d.ts} +0 -1
- package/dist/{heartbeat.js → src/heartbeat.js} +0 -1
- package/dist/{index.d.ts → src/index.d.ts} +0 -1
- package/dist/{index.js → src/index.js} +0 -1
- package/dist/{logger.d.ts → src/logger.d.ts} +0 -1
- package/dist/{logger.js → src/logger.js} +0 -1
- package/dist/{network → src/network}/http.d.ts +0 -1
- package/dist/{network → src/network}/http.js +0 -1
- package/dist/{network → src/network}/local.d.ts +0 -1
- package/dist/{network → src/network}/local.js +9 -1
- package/dist/{network → src/network}/nats.d.ts +0 -1
- package/dist/{network → src/network}/nats.js +0 -1
- package/dist/{network → src/network}/network.d.ts +0 -1
- package/dist/src/network/network.js +1 -0
- package/dist/src/network/postgres.d.ts +49 -0
- package/dist/src/network/postgres.js +155 -0
- package/dist/{network → src/network}/types.d.ts +0 -1
- package/dist/{network → src/network}/types.js +0 -1
- package/dist/{options.d.ts → src/options.d.ts} +0 -1
- package/dist/{options.js → src/options.js} +0 -1
- package/dist/{platform.d.ts → src/platform.d.ts} +0 -1
- package/dist/{platform.js → src/platform.js} +0 -1
- package/dist/{promises.d.ts → src/promises.d.ts} +0 -1
- package/dist/{promises.js → src/promises.js} +0 -1
- package/dist/{registry.d.ts → src/registry.d.ts} +0 -1
- package/dist/{registry.js → src/registry.js} +0 -1
- package/dist/{resonate.d.ts → src/resonate.d.ts} +0 -1
- package/dist/{resonate.js → src/resonate.js} +0 -1
- package/dist/{retries.d.ts → src/retries.d.ts} +0 -1
- package/dist/{retries.js → src/retries.js} +0 -1
- package/dist/{schedules.d.ts → src/schedules.d.ts} +0 -1
- package/dist/{schedules.js → src/schedules.js} +0 -1
- package/dist/{trace.d.ts → src/trace.d.ts} +0 -1
- package/dist/{trace.js → src/trace.js} +0 -1
- package/dist/{types.d.ts → src/types.d.ts} +0 -1
- package/dist/src/types.js +1 -0
- package/dist/{util.d.ts → src/util.d.ts} +0 -1
- package/dist/{util.js → src/util.js} +0 -1
- package/package.json +46 -25
- package/dist/async/context.d.ts.map +0 -1
- package/dist/async/context.js.map +0 -1
- package/dist/async/core.d.ts.map +0 -1
- package/dist/async/core.js.map +0 -1
- package/dist/async/index.d.ts.map +0 -1
- package/dist/async/index.js.map +0 -1
- package/dist/async/resonate.d.ts.map +0 -1
- package/dist/async/resonate.js.map +0 -1
- package/dist/clock.d.ts.map +0 -1
- package/dist/clock.js.map +0 -1
- package/dist/codec.d.ts.map +0 -1
- package/dist/codec.js.map +0 -1
- package/dist/computation.d.ts.map +0 -1
- package/dist/computation.js.map +0 -1
- package/dist/context.d.ts.map +0 -1
- package/dist/context.js.map +0 -1
- package/dist/core.d.ts.map +0 -1
- package/dist/core.js.map +0 -1
- package/dist/coroutine.d.ts.map +0 -1
- package/dist/coroutine.js.map +0 -1
- package/dist/decorator.d.ts.map +0 -1
- package/dist/decorator.js.map +0 -1
- package/dist/encryptor.d.ts.map +0 -1
- package/dist/encryptor.js.map +0 -1
- package/dist/exceptions.d.ts.map +0 -1
- package/dist/exceptions.js.map +0 -1
- package/dist/heartbeat.d.ts.map +0 -1
- package/dist/heartbeat.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/logger.d.ts.map +0 -1
- package/dist/logger.js.map +0 -1
- package/dist/network/http.d.ts.map +0 -1
- package/dist/network/http.js.map +0 -1
- package/dist/network/local.d.ts.map +0 -1
- package/dist/network/local.js.map +0 -1
- package/dist/network/nats.d.ts.map +0 -1
- package/dist/network/nats.js.map +0 -1
- package/dist/network/network.d.ts.map +0 -1
- package/dist/network/network.js +0 -2
- package/dist/network/network.js.map +0 -1
- package/dist/network/types.d.ts.map +0 -1
- package/dist/network/types.js.map +0 -1
- package/dist/options.d.ts.map +0 -1
- package/dist/options.js.map +0 -1
- package/dist/platform.d.ts.map +0 -1
- package/dist/platform.js.map +0 -1
- package/dist/promises.d.ts.map +0 -1
- package/dist/promises.js.map +0 -1
- package/dist/registry.d.ts.map +0 -1
- package/dist/registry.js.map +0 -1
- package/dist/resonate.d.ts.map +0 -1
- package/dist/resonate.js.map +0 -1
- package/dist/retries.d.ts.map +0 -1
- package/dist/retries.js.map +0 -1
- package/dist/schedules.d.ts.map +0 -1
- package/dist/schedules.js.map +0 -1
- package/dist/trace.d.ts.map +0 -1
- package/dist/trace.js.map +0 -1
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -2
- package/dist/types.js.map +0 -1
- package/dist/util.d.ts.map +0 -1
- package/dist/util.js.map +0 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Resonate TypeScript SDK
|
|
4
4
|
|
|
5
|
-
[](https://github.com/resonatehq/resonate-sdk-ts/actions/workflows/ci.yml)
|
|
6
6
|
[](https://codecov.io/gh/resonatehq/resonate-sdk-ts)
|
|
7
7
|
[](https://github.com/resonatehq/resonate-sdk-ts/actions/workflows/dst.yml)
|
|
8
8
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
@@ -267,4 +267,3 @@ export declare function run(ctx: AsyncContext, func: AnyFunc, args: any[]): Prom
|
|
|
267
267
|
*/
|
|
268
268
|
export declare function runWithRetry(makeCtx: (attempt: number) => AsyncContext, func: AnyFunc, args: any[], policy: RetryPolicy, nonRetryableErrors: Array<new (...args: any[]) => Error>, clock: Clock, timeoutAt: number): Promise<Outcome>;
|
|
269
269
|
export {};
|
|
270
|
-
//# sourceMappingURL=context.d.ts.map
|
|
@@ -2,4 +2,3 @@ export { Constant, Exponential, Linear, Never, type RetryPolicy } from "../retri
|
|
|
2
2
|
export { type AnyFunc, type Context, type DetachedHandle, DurablePromise, type Info } from "./context.js";
|
|
3
3
|
export { Core, type Done, type Status, type Suspended } from "./core.js";
|
|
4
4
|
export { Resonate, type ResonateFunc, type ResonateHandle, type ResonateSchedule, } from "./resonate.js";
|
|
5
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -17,4 +17,3 @@ export { Resonate, type ResonateFunc, type ResonateHandle } from "./resonate.js"
|
|
|
17
17
|
export { Constant, Exponential, Linear, Never, type RetryPolicy } from "./retries.js";
|
|
18
18
|
export { awaitThenResumeOrSuspend, blockIsSole, dedupIsSole, type Event, exclusiveLifecycle, isWellFormed, rootSpawn, rpcHasCallee, runHasCallee, spawnIsFirst, type Trace, TraceCollector, terminalIsLast, uniqueSpawn, uniqueTerminal, } from "./trace.js";
|
|
19
19
|
export type { Effects, Func } from "./types.js";
|
|
20
|
-
//# sourceMappingURL=index.d.ts.map
|
|
@@ -13,4 +13,3 @@ export { Registry } from "./registry.js";
|
|
|
13
13
|
export { Resonate } from "./resonate.js";
|
|
14
14
|
export { Constant, Exponential, Linear, Never } from "./retries.js";
|
|
15
15
|
export { awaitThenResumeOrSuspend, blockIsSole, dedupIsSole, exclusiveLifecycle, isWellFormed, rootSpawn, rpcHasCallee, runHasCallee, spawnIsFirst, TraceCollector, terminalIsLast, uniqueSpawn, uniqueTerminal, } from "./trace.js";
|
|
16
|
-
//# sourceMappingURL=index.js.map
|
|
@@ -768,6 +768,15 @@ export class Server {
|
|
|
768
768
|
return { response: this.response("schedule.get", 200, { schedule: this.toScheduleRecord(schedule) }), changes: [] };
|
|
769
769
|
}
|
|
770
770
|
scheduleCreate(now, req) {
|
|
771
|
+
// Mirror the server's create-time validation: every fired promise needs a
|
|
772
|
+
// routing target, so a schedule whose promiseTags lack resonate:target is
|
|
773
|
+
// rejected up front (same status and message as the real server).
|
|
774
|
+
if (!req.data.promiseTags || !("resonate:target" in req.data.promiseTags)) {
|
|
775
|
+
return {
|
|
776
|
+
response: this.response("schedule.create", 400, "promiseTags must include a resonate:target tag"),
|
|
777
|
+
changes: [],
|
|
778
|
+
};
|
|
779
|
+
}
|
|
771
780
|
const existingSchedule = this.schedules.get(req.data.id);
|
|
772
781
|
if (existingSchedule) {
|
|
773
782
|
return {
|
|
@@ -1346,4 +1355,3 @@ export class LocalNetwork {
|
|
|
1346
1355
|
}
|
|
1347
1356
|
}
|
|
1348
1357
|
}
|
|
1349
|
-
//# sourceMappingURL=local.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Logger } from "../logger.js";
|
|
2
|
+
import type { Network } from "./network.js";
|
|
3
|
+
import type { Message, Request, Response } from "./types.js";
|
|
4
|
+
export interface PostgresNetworkConfig {
|
|
5
|
+
/** Postgres connection string (e.g. postgres://user:pass@host:5432/db). */
|
|
6
|
+
connectionString: string;
|
|
7
|
+
/** Worker group; a task targets the group (anycast). Default "default". */
|
|
8
|
+
group?: string;
|
|
9
|
+
/** This process's id; a callback/listener targets it (unicast). Defaults to a random id. */
|
|
10
|
+
pid?: string;
|
|
11
|
+
/** Fallback poll interval (ms) so timers still fire if a NOTIFY is missed. */
|
|
12
|
+
tickMs?: number;
|
|
13
|
+
logger?: Logger;
|
|
14
|
+
}
|
|
15
|
+
export declare class PostgresNetwork implements Network {
|
|
16
|
+
readonly unicast: string;
|
|
17
|
+
readonly anycast: string;
|
|
18
|
+
private readonly connectionString;
|
|
19
|
+
private readonly group;
|
|
20
|
+
private readonly pid;
|
|
21
|
+
private readonly tickMs;
|
|
22
|
+
private readonly logger?;
|
|
23
|
+
private poolPromise?;
|
|
24
|
+
private listenClient?;
|
|
25
|
+
private callbacks;
|
|
26
|
+
private timer?;
|
|
27
|
+
private draining;
|
|
28
|
+
private redrain;
|
|
29
|
+
private stopped;
|
|
30
|
+
constructor(cfg: PostgresNetworkConfig);
|
|
31
|
+
match(target: string): string;
|
|
32
|
+
private pg;
|
|
33
|
+
private getPool;
|
|
34
|
+
init(): Promise<void>;
|
|
35
|
+
stop(): Promise<void>;
|
|
36
|
+
send: <K extends Request["kind"]>(req: Extract<Request, {
|
|
37
|
+
kind: K;
|
|
38
|
+
}>) => Promise<Extract<Response, {
|
|
39
|
+
kind: K;
|
|
40
|
+
}>>;
|
|
41
|
+
recv(callback: (msg: Message) => void): void;
|
|
42
|
+
private deliver;
|
|
43
|
+
/** Fire due timers, then dequeue this node's outbox rows, delivering each. */
|
|
44
|
+
private drain;
|
|
45
|
+
/** Dequeue rows addressed to `address` in batches until the backlog is empty. */
|
|
46
|
+
private dequeue;
|
|
47
|
+
/** Fallback tick so timers and missed NOTIFYs still advance the pump. */
|
|
48
|
+
private scheduleNext;
|
|
49
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
export class PostgresNetwork {
|
|
2
|
+
unicast;
|
|
3
|
+
anycast;
|
|
4
|
+
connectionString;
|
|
5
|
+
group;
|
|
6
|
+
pid;
|
|
7
|
+
tickMs;
|
|
8
|
+
logger;
|
|
9
|
+
// `pg` is loaded lazily (optional peer dependency); the pool is memoized so
|
|
10
|
+
// send() works even before init() resolves (the SDK does not await init()).
|
|
11
|
+
poolPromise;
|
|
12
|
+
listenClient;
|
|
13
|
+
callbacks = [];
|
|
14
|
+
timer;
|
|
15
|
+
draining = false;
|
|
16
|
+
redrain = false;
|
|
17
|
+
stopped = false;
|
|
18
|
+
constructor(cfg) {
|
|
19
|
+
this.connectionString = cfg.connectionString;
|
|
20
|
+
this.group = cfg.group ?? "default";
|
|
21
|
+
this.pid = cfg.pid ?? crypto.randomUUID().replace(/-/g, "");
|
|
22
|
+
this.tickMs = cfg.tickMs ?? 250;
|
|
23
|
+
this.logger = cfg.logger;
|
|
24
|
+
// A task targets the group (anycast); a callback/listener targets this
|
|
25
|
+
// specific process (unicast). The poll:// scheme mirrors the poll
|
|
26
|
+
// adapter's addressing and is what the schema accepts for listeners.
|
|
27
|
+
this.unicast = `poll://uni@${this.group}/${this.pid}`;
|
|
28
|
+
this.anycast = `poll://any@${this.group}`;
|
|
29
|
+
}
|
|
30
|
+
match(target) {
|
|
31
|
+
return `poll://any@${target}`;
|
|
32
|
+
}
|
|
33
|
+
async pg() {
|
|
34
|
+
const m = (await import("pg"));
|
|
35
|
+
const PoolCtor = (m.Pool ?? m.default?.Pool);
|
|
36
|
+
const ClientCtor = (m.Client ?? m.default?.Client);
|
|
37
|
+
return { Pool: PoolCtor, Client: ClientCtor };
|
|
38
|
+
}
|
|
39
|
+
getPool() {
|
|
40
|
+
if (!this.poolPromise) {
|
|
41
|
+
this.poolPromise = this.pg().then((pg) => new pg.Pool({ connectionString: this.connectionString, max: 8 }));
|
|
42
|
+
}
|
|
43
|
+
return this.poolPromise;
|
|
44
|
+
}
|
|
45
|
+
async init() {
|
|
46
|
+
const pool = await this.getPool();
|
|
47
|
+
// A dedicated connection held open for LISTEN; node-postgres surfaces
|
|
48
|
+
// notifications as 'notification' events. The schema NOTIFYs on a channel
|
|
49
|
+
// derived from each row's address, so listen on this node's addresses.
|
|
50
|
+
const pg = await this.pg();
|
|
51
|
+
this.listenClient = new pg.Client({ connectionString: this.connectionString });
|
|
52
|
+
await this.listenClient.connect();
|
|
53
|
+
this.listenClient.on("notification", () => void this.drain());
|
|
54
|
+
this.listenClient.on("error", (err) => this.logger?.warn({ component: "network", error: err.message }, "postgres listen error"));
|
|
55
|
+
for (const address of [this.unicast, this.anycast]) {
|
|
56
|
+
const { rows } = await pool.query("SELECT resonate.outbox_channel($1) AS channel", [address]);
|
|
57
|
+
await this.listenClient.query(`LISTEN "${rows[0].channel}"`);
|
|
58
|
+
}
|
|
59
|
+
this.scheduleNext();
|
|
60
|
+
await this.drain();
|
|
61
|
+
this.logger?.info({ component: "network", group: this.group, pid: this.pid }, "postgres network initialized");
|
|
62
|
+
}
|
|
63
|
+
async stop() {
|
|
64
|
+
this.stopped = true;
|
|
65
|
+
if (this.timer)
|
|
66
|
+
clearTimeout(this.timer);
|
|
67
|
+
try {
|
|
68
|
+
await this.listenClient?.end();
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
/* already closed */
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
await (await this.poolPromise)?.end();
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
/* already closed */
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
send = async (req) => {
|
|
81
|
+
const pool = await this.getPool();
|
|
82
|
+
const { rows } = await pool.query("SELECT resonate.resonate_rpc($1::jsonb) AS res", [JSON.stringify(req)]);
|
|
83
|
+
return rows[0].res;
|
|
84
|
+
};
|
|
85
|
+
recv(callback) {
|
|
86
|
+
this.callbacks.push(callback);
|
|
87
|
+
}
|
|
88
|
+
// --- runtime pump ---------------------------------------------------------
|
|
89
|
+
deliver(msg) {
|
|
90
|
+
for (const cb of this.callbacks)
|
|
91
|
+
cb(msg);
|
|
92
|
+
}
|
|
93
|
+
/** Fire due timers, then dequeue this node's outbox rows, delivering each. */
|
|
94
|
+
async drain() {
|
|
95
|
+
if (this.stopped)
|
|
96
|
+
return;
|
|
97
|
+
if (this.draining) {
|
|
98
|
+
this.redrain = true; // coalesce concurrent wakes into one pass
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this.draining = true;
|
|
102
|
+
try {
|
|
103
|
+
const pool = await this.getPool();
|
|
104
|
+
// No-arg overload: timers fire on database time (clock_timestamp), so
|
|
105
|
+
// every node shares one time authority regardless of local clock skew,
|
|
106
|
+
// and concurrent calls from multiple nodes are safe (advisory locks,
|
|
107
|
+
// idempotent handlers).
|
|
108
|
+
await pool.query("SELECT resonate.process_timeouts()");
|
|
109
|
+
// Addressed, destructive dequeue (FOR UPDATE SKIP LOCKED): this node
|
|
110
|
+
// only consumes rows addressed to it, and same-group nodes split the
|
|
111
|
+
// anycast stream. Delivery is at-least-once: if we crash after
|
|
112
|
+
// dequeuing an execute row but before the task is claimed, the task
|
|
113
|
+
// stays pending and the server re-emits it on the task retry timeout
|
|
114
|
+
// (stale duplicates fail acquire with a 409, which the SDK tolerates).
|
|
115
|
+
// Execute rows target the task's target address (anycast by default);
|
|
116
|
+
// unblock rows target the listener's address (this node's unicast).
|
|
117
|
+
for (const address of [this.anycast, this.unicast]) {
|
|
118
|
+
await this.dequeue(pool, "SELECT task_id, version FROM resonate.dequeue_execute($1, $2)", address, (r) => this.deliver({ kind: "execute", head: {}, data: { task: { id: r.task_id, version: r.version } } }));
|
|
119
|
+
}
|
|
120
|
+
await this.dequeue(pool, "SELECT promise FROM resonate.dequeue_unblock($1, $2)", this.unicast, (r) => this.deliver({ kind: "unblock", head: {}, data: { promise: r.promise } }));
|
|
121
|
+
}
|
|
122
|
+
catch (err) {
|
|
123
|
+
if (!this.stopped) {
|
|
124
|
+
this.logger?.warn({ component: "network", error: err.message }, "postgres drain error");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
this.draining = false;
|
|
129
|
+
if (this.redrain) {
|
|
130
|
+
this.redrain = false;
|
|
131
|
+
void this.drain();
|
|
132
|
+
}
|
|
133
|
+
this.scheduleNext();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
/** Dequeue rows addressed to `address` in batches until the backlog is empty. */
|
|
137
|
+
async dequeue(pool, sql, address, deliver) {
|
|
138
|
+
const limit = 100;
|
|
139
|
+
while (true) {
|
|
140
|
+
const { rows } = await pool.query(sql, [address, limit]);
|
|
141
|
+
for (const r of rows)
|
|
142
|
+
deliver(r);
|
|
143
|
+
if (rows.length < limit)
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** Fallback tick so timers and missed NOTIFYs still advance the pump. */
|
|
148
|
+
scheduleNext() {
|
|
149
|
+
if (this.stopped)
|
|
150
|
+
return;
|
|
151
|
+
if (this.timer)
|
|
152
|
+
clearTimeout(this.timer);
|
|
153
|
+
this.timer = setTimeout(() => void this.drain(), this.tickMs);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -1277,4 +1277,3 @@ export declare function isScheduleRecord(val: unknown): val is ScheduleRecord;
|
|
|
1277
1277
|
export declare function isExecuteMsg(val: unknown): val is ExecuteMsg;
|
|
1278
1278
|
export declare function isUnblockMsg(val: unknown): val is UnblockMsg;
|
|
1279
1279
|
export declare function isMessage(val: unknown): val is Message;
|
|
1280
|
-
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|