@johannc-dev/zyrk 0.2.0 → 0.2.2
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 +8 -1
- package/dist/cli.d.ts +6 -1
- package/dist/cli.js +44 -8
- package/dist/client.d.ts +17 -4
- package/dist/client.js +515 -18
- package/dist/contracts.d.ts +16 -0
- package/dist/contracts.js +14 -0
- package/dist/errors.d.ts +9 -1
- package/dist/errors.js +6 -1
- package/dist/export.d.ts +27 -8
- package/dist/export.js +149 -19
- package/dist/hourfile.d.ts +3 -1
- package/dist/hourfile.js +7 -0
- package/dist/limits.d.ts +16 -2
- package/dist/limits.js +2 -0
- package/dist/local-data.d.ts +21 -4
- package/dist/local-data.js +52 -1
- package/dist/local-replay.d.ts +5 -5
- package/dist/local-replay.js +4 -3
- package/dist/local-storage.d.ts +2 -2
- package/dist/local.d.ts +2 -2
- package/dist/local.js +166 -35
- package/dist/sandbox-bootstrap.js +231 -9
- package/dist/sdk.d.ts +260 -29
- package/dist/sdk.js +14 -0
- package/dist/stream.d.ts +52 -4
- package/dist/stream.js +111 -13
- package/package.json +3 -3
package/dist/client.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Cause, Config, Context, Effect, Exit, Layer, Option, Redacted, Schema, } from "effect";
|
|
2
|
-
import { decode, Job, Json, ProgramSummary, Started, toJobResult, } from "./contracts.js";
|
|
1
|
+
import { Cause, Config, Context, Effect, Exit, Layer, Option, Redacted, Result, Schema, } from "effect";
|
|
2
|
+
import { decode, Job, Json, ProgramDeleted, ProgramSummary, Started, toJobResult, } from "./contracts.js";
|
|
3
3
|
import { ApiError, ConfigError, DecodeError, ExecutionEnded, ExecutionFailed, Failure, messageOf, NetworkError, WaitTimeout, } from "./errors.js";
|
|
4
4
|
export * from "./errors.js";
|
|
5
5
|
const ServerError = Schema.Struct({
|
|
@@ -38,9 +38,50 @@ const parseJson = (text) => {
|
|
|
38
38
|
};
|
|
39
39
|
const segment = encodeURIComponent;
|
|
40
40
|
const isFinal = (r) => r.status !== "queued" && r.status !== "running";
|
|
41
|
+
const MAX_HOLD_MS = 10_000;
|
|
42
|
+
const POLL_MS = 1_000;
|
|
43
|
+
const FOLLOW_SLACK_MS = 25;
|
|
44
|
+
const MAX_WAIT_IDS = 100;
|
|
45
|
+
const WAITED_HEADER = "x-functions-waited";
|
|
46
|
+
const rhythmFrom = (start) => ({
|
|
47
|
+
anchor: start,
|
|
48
|
+
last: start,
|
|
49
|
+
rtt: undefined,
|
|
50
|
+
});
|
|
51
|
+
const periodOf = (rhythm) => POLL_MS + (rhythm.rtt ?? 0);
|
|
52
|
+
const tickFrom = (rhythm, t) => {
|
|
53
|
+
const period = periodOf(rhythm);
|
|
54
|
+
const from = Math.max(t, rhythm.last + 1);
|
|
55
|
+
const k = Math.max(0, Math.ceil((from - rhythm.anchor) / period));
|
|
56
|
+
return rhythm.anchor + k * period;
|
|
57
|
+
};
|
|
58
|
+
const tickBefore = (rhythm, t) => t < rhythm.anchor
|
|
59
|
+
? -Infinity
|
|
60
|
+
: rhythm.anchor +
|
|
61
|
+
Math.floor((t - rhythm.anchor) / periodOf(rhythm)) * periodOf(rhythm);
|
|
62
|
+
const answered = (rhythm, sentAt, now, tick = sentAt) => {
|
|
63
|
+
rhythm.rtt = now - sentAt;
|
|
64
|
+
rhythm.anchor = tick + rhythm.rtt + POLL_MS;
|
|
65
|
+
};
|
|
66
|
+
const planFor = (rhythm, sentAt, rtt, offset, cap) => {
|
|
67
|
+
if (!(cap >= POLL_MS))
|
|
68
|
+
return undefined;
|
|
69
|
+
const cappedAt = Math.min(cap, MAX_HOLD_MS);
|
|
70
|
+
const tick = tickBefore(rhythm, sentAt + rtt + cappedAt - offset);
|
|
71
|
+
const waitMs = tick + offset - sentAt - rtt;
|
|
72
|
+
return tick > rhythm.last && waitMs >= POLL_MS
|
|
73
|
+
? { waitMs, tick }
|
|
74
|
+
: { waitMs: cappedAt, tick: undefined };
|
|
75
|
+
};
|
|
76
|
+
const transient = (error) => error instanceof NetworkError ||
|
|
77
|
+
(error instanceof ApiError && [502, 503, 504].includes(error.status));
|
|
78
|
+
const sleepUntil = (t) => Effect.suspend(() => {
|
|
79
|
+
const ms = t - Date.now();
|
|
80
|
+
return ms > 0 ? Effect.sleep(ms) : Effect.void;
|
|
81
|
+
});
|
|
41
82
|
export const make = (config) => {
|
|
42
83
|
const base = `${config.url.replace(/\/$/, "")}/v1/functions/`;
|
|
43
|
-
const
|
|
84
|
+
const send = Effect.fnUntraced(function* (method, path, body) {
|
|
44
85
|
const url = base + path;
|
|
45
86
|
const response = yield* Effect.tryPromise({
|
|
46
87
|
try: async (signal) => {
|
|
@@ -57,6 +98,7 @@ export const make = (config) => {
|
|
|
57
98
|
return {
|
|
58
99
|
status: response.status,
|
|
59
100
|
ok: response.ok,
|
|
101
|
+
waited: response.headers?.get?.(WAITED_HEADER) === "1",
|
|
60
102
|
text: await response.text(),
|
|
61
103
|
};
|
|
62
104
|
},
|
|
@@ -74,15 +116,53 @@ export const make = (config) => {
|
|
|
74
116
|
what: `${method} ${path}`,
|
|
75
117
|
message: "Response body is not JSON",
|
|
76
118
|
});
|
|
77
|
-
|
|
119
|
+
const json = yield* decode(Json, `${method} ${path}`)(parsed.value);
|
|
120
|
+
return { json, waited: response.waited };
|
|
121
|
+
});
|
|
122
|
+
const request = (method, path, body) => send(method, path, body).pipe(Effect.map((r) => r.json));
|
|
123
|
+
const executionAt = (id, query = "") => send("GET", `executions/${segment(id)}${query}`).pipe(Effect.flatMap((reply) => decode(Job, "execution")(reply.json).pipe(Effect.map((job) => ({ job, waited: reply.waited })))));
|
|
124
|
+
const execution = (id) => executionAt(id).pipe(Effect.map((r) => r.job));
|
|
125
|
+
const followWith = (id, cursor, query) => Effect.suspend(() => executionAt(cursor.current, query()).pipe(Effect.flatMap(({ job, waited }) => {
|
|
126
|
+
if (job.status !== "continued" || cursor.hops >= 10_000)
|
|
127
|
+
return Effect.succeed({
|
|
128
|
+
result: { ...toJobResult(job), id },
|
|
129
|
+
waited,
|
|
130
|
+
});
|
|
131
|
+
cursor.current = job.nextExecutionId;
|
|
132
|
+
cursor.hops++;
|
|
133
|
+
return followWith(id, cursor, query);
|
|
134
|
+
})));
|
|
135
|
+
const follow = (id, cursor) => followWith(id, cursor, () => "").pipe(Effect.map((r) => r.result));
|
|
136
|
+
const cursorAt = (id) => ({ current: id, hops: 0 });
|
|
137
|
+
const result = (id) => follow(id, cursorAt(id));
|
|
138
|
+
const deadlineOf = (options) => options.timeoutMs === undefined ? Infinity : Date.now() + options.timeoutMs;
|
|
139
|
+
const waitQuery = (plan) => plan === undefined ? "" : `?waitMs=${plan.waitMs}`;
|
|
140
|
+
const guarded = (check, held, deadline) => held && deadline !== Infinity
|
|
141
|
+
? check.pipe(Effect.timeoutOption(Math.max(0, deadline - Date.now())))
|
|
142
|
+
: check.pipe(Effect.map(Option.some));
|
|
143
|
+
const recover = (rhythm, failedAt, deadline, check, one = rhythm.rtt, offset = 0) => Effect.gen(function* () {
|
|
144
|
+
if (failedAt >= deadline)
|
|
145
|
+
return Option.none();
|
|
146
|
+
const safe = failedAt + POLL_MS;
|
|
147
|
+
const due = one === undefined
|
|
148
|
+
? undefined
|
|
149
|
+
: tickFrom(rhythm, failedAt - one - offset);
|
|
150
|
+
const at = Math.min(due === undefined ? safe : Math.max(due + offset, failedAt), deadline);
|
|
151
|
+
const tick = due !== undefined && due < deadline ? due : undefined;
|
|
152
|
+
yield* sleepUntil(at);
|
|
153
|
+
rhythm.last = Math.max(rhythm.last, tick ?? at);
|
|
154
|
+
const sentAt = Date.now();
|
|
155
|
+
const first = yield* Effect.result(check);
|
|
156
|
+
if (Result.isSuccess(first))
|
|
157
|
+
return Option.some({ value: first.success, sentAt, tick });
|
|
158
|
+
if (!transient(first.failure) || sentAt >= safe || Date.now() >= deadline)
|
|
159
|
+
return yield* Effect.fail(first.failure);
|
|
160
|
+
yield* sleepUntil(Math.min(safe, deadline));
|
|
161
|
+
const again = Date.now();
|
|
162
|
+
rhythm.last = Math.max(rhythm.last, again);
|
|
163
|
+
const value = yield* check;
|
|
164
|
+
return Option.some({ value, sentAt: again, tick: undefined });
|
|
78
165
|
});
|
|
79
|
-
const execution = (id) => request("GET", `executions/${segment(id)}`).pipe(Effect.flatMap(decode(Job, "execution")));
|
|
80
|
-
const result = (id) => {
|
|
81
|
-
const follow = (current, hops) => execution(current).pipe(Effect.flatMap((job) => job.status === "continued" && hops < 10_000
|
|
82
|
-
? follow(job.nextExecutionId, hops + 1)
|
|
83
|
-
: Effect.succeed({ ...toJobResult(job), id })));
|
|
84
|
-
return follow(id, 0);
|
|
85
|
-
};
|
|
86
166
|
const poll = (check, options, onTimeout) => {
|
|
87
167
|
const deadline = options.timeoutMs === undefined
|
|
88
168
|
? Infinity
|
|
@@ -98,7 +178,10 @@ export const make = (config) => {
|
|
|
98
178
|
})));
|
|
99
179
|
return step;
|
|
100
180
|
};
|
|
101
|
-
const wait = (id, options = {}) =>
|
|
181
|
+
const wait = (id, options = {}) => Effect.suspend(() => options.pollMs === undefined
|
|
182
|
+
? waitHeld(id, cursorAt(id), options)
|
|
183
|
+
: waitFrom(id, cursorAt(id), options));
|
|
184
|
+
const outcome = (id, r) => {
|
|
102
185
|
switch (r.status) {
|
|
103
186
|
case "succeeded":
|
|
104
187
|
return Effect.succeed(Option.some(r.result));
|
|
@@ -112,17 +195,427 @@ export const make = (config) => {
|
|
|
112
195
|
case "running":
|
|
113
196
|
return Effect.succeed(Option.none());
|
|
114
197
|
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
198
|
+
};
|
|
199
|
+
const timedOut = (id, options) => Effect.fail(new WaitTimeout({ executionId: id, timeoutMs: options.timeoutMs ?? 0 }));
|
|
200
|
+
const waitFrom = (id, cursor, options) => poll(follow(id, cursor).pipe(Effect.flatMap((r) => outcome(id, r))), options, () => timedOut(id, options));
|
|
201
|
+
const holdOne = (id, cursor, options) => Effect.gen(function* () {
|
|
202
|
+
const start = Date.now();
|
|
203
|
+
const deadline = deadlineOf(options);
|
|
204
|
+
const rhythm = rhythmFrom(start);
|
|
205
|
+
const plain = followWith(id, cursor, () => "");
|
|
206
|
+
let failures = 0;
|
|
207
|
+
let polling = false;
|
|
208
|
+
let at = start;
|
|
209
|
+
let tick = start;
|
|
210
|
+
let sentAt = start;
|
|
211
|
+
let plan;
|
|
212
|
+
const planNow = () => {
|
|
213
|
+
sentAt = Date.now();
|
|
214
|
+
const rtt = rhythm.rtt ?? 0;
|
|
215
|
+
plan = polling
|
|
216
|
+
? undefined
|
|
217
|
+
: planFor(rhythm, sentAt, rtt, 0, deadline - sentAt - rtt);
|
|
218
|
+
};
|
|
219
|
+
for (;;) {
|
|
220
|
+
yield* sleepUntil(at);
|
|
221
|
+
if (tick !== undefined)
|
|
222
|
+
rhythm.last = Math.max(rhythm.last, tick);
|
|
223
|
+
planNow();
|
|
224
|
+
const held = plan !== undefined;
|
|
225
|
+
const read = Effect.suspend(() => executionAt(cursor.current, waitQuery(plan)));
|
|
226
|
+
const tried = yield* Effect.result(guarded(read, held, deadline));
|
|
227
|
+
let result;
|
|
228
|
+
let now;
|
|
229
|
+
if (Result.isFailure(tried)) {
|
|
230
|
+
if (!held || !transient(tried.failure))
|
|
231
|
+
return yield* Effect.fail(tried.failure);
|
|
232
|
+
if (++failures >= 2)
|
|
233
|
+
polling = true;
|
|
234
|
+
const recovered = yield* recover(rhythm, Date.now(), deadline, plain);
|
|
235
|
+
if (Option.isNone(recovered))
|
|
236
|
+
return { latest: undefined };
|
|
237
|
+
const { value, sentAt: s, tick: due } = recovered.value;
|
|
238
|
+
now = Date.now();
|
|
239
|
+
result = value.result;
|
|
240
|
+
if (due !== undefined)
|
|
241
|
+
answered(rhythm, s, now, due);
|
|
242
|
+
else if (rhythm.rtt === undefined)
|
|
243
|
+
rhythm.rtt = now - s;
|
|
244
|
+
const lag = due === undefined ? 0 : s - due;
|
|
245
|
+
tick = tickFrom(rhythm, now - lag);
|
|
246
|
+
at = Math.max(tick, now);
|
|
247
|
+
}
|
|
248
|
+
else if (Option.isNone(tried.success)) {
|
|
249
|
+
const { result: last } = yield* plain;
|
|
250
|
+
return { latest: last };
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
const { job, waited } = tried.success.value;
|
|
254
|
+
now = Date.now();
|
|
255
|
+
const asked = plan?.waitMs ?? 0;
|
|
256
|
+
const heldMs = now - sentAt;
|
|
257
|
+
const full = asked > 0 && heldMs >= Math.max(POLL_MS, 0.9 * asked);
|
|
258
|
+
if (job.status === "continued" && cursor.hops < 10_000) {
|
|
259
|
+
cursor.current = job.nextExecutionId;
|
|
260
|
+
cursor.hops++;
|
|
261
|
+
if (!waited || heldMs <= (rhythm.rtt ?? 0) + FOLLOW_SLACK_MS) {
|
|
262
|
+
if (tick !== undefined) {
|
|
263
|
+
if (!waited)
|
|
264
|
+
rhythm.rtt = heldMs;
|
|
265
|
+
rhythm.anchor = now + (rhythm.rtt ?? 0) + POLL_MS;
|
|
266
|
+
}
|
|
267
|
+
at = now;
|
|
268
|
+
}
|
|
269
|
+
else {
|
|
270
|
+
failures = 0;
|
|
271
|
+
const rtt = rhythm.rtt ?? 0;
|
|
272
|
+
tick = tickFrom(rhythm, now - rtt);
|
|
273
|
+
at = tick + rtt;
|
|
274
|
+
rhythm.anchor = at + rtt + POLL_MS;
|
|
275
|
+
}
|
|
276
|
+
if (at >= deadline) {
|
|
277
|
+
at = deadline;
|
|
278
|
+
tick = undefined;
|
|
279
|
+
}
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
result = { ...toJobResult(job), id };
|
|
283
|
+
if (!waited) {
|
|
284
|
+
if (tick !== undefined)
|
|
285
|
+
answered(rhythm, sentAt, now);
|
|
286
|
+
else if (rhythm.rtt === undefined)
|
|
287
|
+
rhythm.rtt = heldMs;
|
|
288
|
+
tick = at = tickFrom(rhythm, now);
|
|
289
|
+
}
|
|
290
|
+
else if (!full) {
|
|
291
|
+
rhythm.anchor = now + POLL_MS;
|
|
292
|
+
tick = at = rhythm.anchor;
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
failures = 0;
|
|
296
|
+
if (rhythm.rtt === undefined) {
|
|
297
|
+
rhythm.rtt = Math.max(0, heldMs - asked);
|
|
298
|
+
tick = at = tickFrom(rhythm, now);
|
|
299
|
+
}
|
|
300
|
+
else if (plan?.tick !== undefined) {
|
|
301
|
+
tick = plan.tick;
|
|
302
|
+
at = Math.max(now, plan.tick);
|
|
303
|
+
}
|
|
304
|
+
else
|
|
305
|
+
tick = at = tickFrom(rhythm, now);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (isFinal(result) || now >= deadline)
|
|
309
|
+
return { latest: result };
|
|
310
|
+
if (at >= deadline) {
|
|
311
|
+
at = deadline;
|
|
312
|
+
tick = undefined;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
const waitHeld = (id, cursor, options) => holdOne(id, cursor, options).pipe(Effect.flatMap(({ latest }) => latest === undefined
|
|
317
|
+
? timedOut(id, options)
|
|
318
|
+
: outcome(id, latest).pipe(Effect.flatMap(Option.match({
|
|
319
|
+
onSome: Effect.succeed,
|
|
320
|
+
onNone: () => timedOut(id, options),
|
|
321
|
+
})))));
|
|
322
|
+
const waitAll = (ids, options = {}) => options.pollMs === undefined
|
|
323
|
+
? waitAllHeld(ids, options)
|
|
324
|
+
: waitAllPolled(ids, options);
|
|
325
|
+
const waitAllPolled = (ids, options) => Effect.suspend(() => {
|
|
120
326
|
const latest = new Map();
|
|
327
|
+
const entries = ids.map((id) => ({ id, cursor: cursorAt(id) }));
|
|
121
328
|
const snapshot = () => ids.map((id) => latest.get(id));
|
|
122
|
-
const check = Effect.forEach(
|
|
329
|
+
const check = Effect.forEach(entries, ({ id, cursor }) => follow(id, cursor).pipe(Effect.tap((r) => Effect.sync(() => latest.set(id, r)))), { concurrency: 8 }).pipe(Effect.map(() => ids.every((id) => isFinal(latest.get(id)))
|
|
123
330
|
? Option.some(snapshot())
|
|
124
331
|
: Option.none()));
|
|
125
332
|
return poll(check, options, () => Effect.sync(snapshot));
|
|
333
|
+
});
|
|
334
|
+
const waitAllHeld = (ids, options) => Effect.suspend(() => {
|
|
335
|
+
if (ids.length === 1) {
|
|
336
|
+
const id = ids[0];
|
|
337
|
+
const cursor = cursorAt(id);
|
|
338
|
+
return holdOne(id, cursor, options).pipe(Effect.flatMap(({ latest }) => latest === undefined ? follow(id, cursor) : Effect.succeed(latest)), Effect.map((r) => [r]));
|
|
339
|
+
}
|
|
340
|
+
return waitMany(ids, options);
|
|
341
|
+
});
|
|
342
|
+
const waitMany = (ids, options) => Effect.gen(function* () {
|
|
343
|
+
const start = Date.now();
|
|
344
|
+
const deadline = deadlineOf(options);
|
|
345
|
+
const rhythm = rhythmFrom(start);
|
|
346
|
+
const latest = new Map();
|
|
347
|
+
const entries = ids.map((id) => ({ id, cursor: cursorAt(id) }));
|
|
348
|
+
const snapshot = () => ids.map((id) => latest.get(id));
|
|
349
|
+
const allFinal = () => ids.every((id) => isFinal(latest.get(id)));
|
|
350
|
+
const seen = (entry, r) => {
|
|
351
|
+
latest.set(entry.id, r);
|
|
352
|
+
entry.readAt = entry.cursor.current;
|
|
353
|
+
};
|
|
354
|
+
let sent = 0;
|
|
355
|
+
const pollingSent = (t) => Math.max(Math.min(ids.length, 8), (ids.length * (t - start)) / periodOf(rhythm));
|
|
356
|
+
let rtt1;
|
|
357
|
+
const pool = (walks) => {
|
|
358
|
+
const slots = Array(Math.min(8, entries.length)).fill(0);
|
|
359
|
+
const starts = entries.map((_, j) => {
|
|
360
|
+
const i = slots.indexOf(Math.min(...slots));
|
|
361
|
+
const startsAt = slots[i];
|
|
362
|
+
slots[i] = startsAt + (1 + (walks?.[j] ?? 0)) * (rtt1 ?? 0);
|
|
363
|
+
return startsAt;
|
|
364
|
+
});
|
|
365
|
+
return { starts, length: Math.max(0, ...slots) };
|
|
366
|
+
};
|
|
367
|
+
const roundTime = (walks) => pool(walks).length;
|
|
368
|
+
const roundEnded = (sentAt, now, tick = sentAt) => {
|
|
369
|
+
answered(rhythm, sentAt, now, tick);
|
|
370
|
+
rhythm.rtt = roundTime();
|
|
371
|
+
};
|
|
372
|
+
const offsetOf = () => Math.max(0, (rhythm.rtt ?? 0) - (rtt1 ?? 0));
|
|
373
|
+
const round = (list, query) => Effect.suspend(() => {
|
|
374
|
+
sent += list.length;
|
|
375
|
+
return Effect.forEach(list, (entry) => Effect.suspend(() => {
|
|
376
|
+
const t = Date.now();
|
|
377
|
+
return followWith(entry.id, entry.cursor, query).pipe(Effect.tap((r) => Effect.sync(() => seen(entry, r.result))), Effect.map((r) => ({
|
|
378
|
+
waited: r.waited,
|
|
379
|
+
took: Date.now() - t,
|
|
380
|
+
})));
|
|
381
|
+
}), { concurrency: 8 });
|
|
382
|
+
}).pipe(Effect.map((reads) => {
|
|
383
|
+
for (const r of reads)
|
|
384
|
+
rtt1 = Math.min(rtt1 ?? Infinity, r.took);
|
|
385
|
+
return reads.some((r) => r.waited);
|
|
386
|
+
}));
|
|
387
|
+
const plainRound = () => round(entries, () => "");
|
|
388
|
+
const fresh = Effect.suspend(() => {
|
|
389
|
+
const stale = entries.filter((e) => e.readAt !== e.cursor.current);
|
|
390
|
+
return stale.length === 0
|
|
391
|
+
? Effect.sync(snapshot)
|
|
392
|
+
: round(stale, () => "").pipe(Effect.map(snapshot));
|
|
393
|
+
});
|
|
394
|
+
const multi = (waitMs) => {
|
|
395
|
+
const distinct = [...new Set(entries.map((e) => e.cursor.current))];
|
|
396
|
+
const chunks = [];
|
|
397
|
+
for (let i = 0; i < distinct.length; i += MAX_WAIT_IDS)
|
|
398
|
+
chunks.push(distinct.slice(i, i + MAX_WAIT_IDS));
|
|
399
|
+
sent += chunks.length;
|
|
400
|
+
return Effect.forEach(chunks, (chunk) => send("POST", "executions/wait", { ids: chunk, waitMs }).pipe(Effect.flatMap((reply) => readMulti(chunk, reply))), { concurrency: 8 }).pipe(Effect.catchIf((e) => e instanceof ApiError && [400, 404, 405].includes(e.status), () => Effect.succeed(null)), Effect.flatMap((answers) => {
|
|
401
|
+
if (answers === null)
|
|
402
|
+
return Effect.succeed(null);
|
|
403
|
+
const records = new Map();
|
|
404
|
+
for (const answer of answers)
|
|
405
|
+
for (const [id, record] of answer.records)
|
|
406
|
+
records.set(id, record);
|
|
407
|
+
let advanced = false;
|
|
408
|
+
let progressed = false;
|
|
409
|
+
const walks = entries.map(() => 0);
|
|
410
|
+
for (const [j, entry] of entries.entries()) {
|
|
411
|
+
const { cursor } = entry;
|
|
412
|
+
const record = records.get(cursor.current);
|
|
413
|
+
if (record instanceof ApiError)
|
|
414
|
+
return Effect.fail(record);
|
|
415
|
+
if (record.status === "continued" && cursor.hops < 10_000) {
|
|
416
|
+
cursor.current = record.nextExecutionId;
|
|
417
|
+
cursor.hops++;
|
|
418
|
+
advanced = true;
|
|
419
|
+
walks[j] = 1;
|
|
420
|
+
}
|
|
421
|
+
else {
|
|
422
|
+
const r = { ...toJobResult(record), id: entry.id };
|
|
423
|
+
const before = latest.get(entry.id);
|
|
424
|
+
if (before !== undefined && !isFinal(before) && isFinal(r))
|
|
425
|
+
progressed = true;
|
|
426
|
+
seen(entry, r);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return Effect.succeed({
|
|
430
|
+
waited: answers.every((a) => a.waited),
|
|
431
|
+
advanced,
|
|
432
|
+
progressed,
|
|
433
|
+
walks,
|
|
434
|
+
});
|
|
435
|
+
}));
|
|
436
|
+
};
|
|
437
|
+
if (ids.length === 0)
|
|
438
|
+
return [];
|
|
439
|
+
let mode = "probe";
|
|
440
|
+
let failures = 0;
|
|
441
|
+
let at = start;
|
|
442
|
+
let tick = start;
|
|
443
|
+
let capped = false;
|
|
444
|
+
let current = start;
|
|
445
|
+
let onRound = true;
|
|
446
|
+
let following = false;
|
|
447
|
+
let roundWalks = entries.map(() => 0);
|
|
448
|
+
let nextWalks;
|
|
449
|
+
for (;;) {
|
|
450
|
+
yield* sleepUntil(at);
|
|
451
|
+
if (tick !== undefined) {
|
|
452
|
+
rhythm.last = current = tick;
|
|
453
|
+
roundWalks = nextWalks ?? entries.map(() => 0);
|
|
454
|
+
nextWalks = undefined;
|
|
455
|
+
}
|
|
456
|
+
onRound = tick !== undefined || (following && onRound);
|
|
457
|
+
following = false;
|
|
458
|
+
const checkAt = Date.now();
|
|
459
|
+
const offset = capped ? offsetOf() : 0;
|
|
460
|
+
let now;
|
|
461
|
+
if (mode !== "multi") {
|
|
462
|
+
const waited = yield* round(entries, mode === "probe" ? () => "?waitMs=0" : () => "");
|
|
463
|
+
now = Date.now();
|
|
464
|
+
if (tick !== undefined)
|
|
465
|
+
roundEnded(checkAt, now);
|
|
466
|
+
if (mode === "probe" && waited)
|
|
467
|
+
mode = "multi";
|
|
468
|
+
if (allFinal() || now >= deadline)
|
|
469
|
+
return yield* fresh;
|
|
470
|
+
tick = tickFrom(rhythm, now);
|
|
471
|
+
at = tick;
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
const one = rtt1 ?? 0;
|
|
475
|
+
const plan = planFor(rhythm, checkAt, one, offset, deadline - checkAt - one);
|
|
476
|
+
const tried = yield* Effect.result(guarded(multi(plan?.waitMs ?? 0), plan !== undefined, deadline));
|
|
477
|
+
now = Date.now();
|
|
478
|
+
if (Result.isFailure(tried)) {
|
|
479
|
+
if (plan === undefined || !transient(tried.failure))
|
|
480
|
+
return yield* Effect.fail(tried.failure);
|
|
481
|
+
if (++failures >= 2)
|
|
482
|
+
mode = "rounds";
|
|
483
|
+
const late = offsetOf();
|
|
484
|
+
const recovered = yield* recover(rhythm, now, deadline, multi(0), one, late);
|
|
485
|
+
if (Option.isNone(recovered))
|
|
486
|
+
return yield* fresh;
|
|
487
|
+
now = Date.now();
|
|
488
|
+
const { value: r, sentAt, tick: due } = recovered.value;
|
|
489
|
+
if (r === null) {
|
|
490
|
+
mode = "rounds";
|
|
491
|
+
yield* plainRound();
|
|
492
|
+
now = Date.now();
|
|
493
|
+
}
|
|
494
|
+
if (due !== undefined)
|
|
495
|
+
rhythm.anchor = due + roundTime() + POLL_MS;
|
|
496
|
+
if ((r === null || !r.advanced) && allFinal())
|
|
497
|
+
return yield* fresh;
|
|
498
|
+
if (now >= deadline)
|
|
499
|
+
return yield* fresh;
|
|
500
|
+
if (r !== null && r.advanced) {
|
|
501
|
+
tick = undefined;
|
|
502
|
+
at = now;
|
|
503
|
+
following = due !== undefined;
|
|
504
|
+
}
|
|
505
|
+
else {
|
|
506
|
+
const lag = due === undefined ? 0 : sentAt - (due + late);
|
|
507
|
+
tick = tickFrom(rhythm, now - lag);
|
|
508
|
+
at = Math.max(tick, now);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
else if (Option.isNone(tried.success)) {
|
|
512
|
+
yield* multi(0);
|
|
513
|
+
return yield* fresh;
|
|
514
|
+
}
|
|
515
|
+
else if (tried.success.value === null) {
|
|
516
|
+
mode = "rounds";
|
|
517
|
+
yield* plainRound();
|
|
518
|
+
const end = Date.now();
|
|
519
|
+
roundEnded(now, end);
|
|
520
|
+
if (allFinal() || end >= deadline)
|
|
521
|
+
return yield* fresh;
|
|
522
|
+
tick = tickFrom(rhythm, end);
|
|
523
|
+
at = tick;
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
const r = tried.success.value;
|
|
527
|
+
if (!r.advanced && allFinal())
|
|
528
|
+
return yield* fresh;
|
|
529
|
+
if (now >= deadline)
|
|
530
|
+
return yield* fresh;
|
|
531
|
+
const asked = plan?.waitMs ?? 0;
|
|
532
|
+
const heldMs = now - checkAt;
|
|
533
|
+
const full = r.waited && asked > 0 && heldMs >= Math.max(POLL_MS, 0.9 * asked);
|
|
534
|
+
if (full || (r.waited && (r.progressed || r.advanced)))
|
|
535
|
+
failures = 0;
|
|
536
|
+
capped = !r.waited;
|
|
537
|
+
const after = capped ? offsetOf() : 0;
|
|
538
|
+
const walked = r.advanced && r.waited && heldMs > one + FOLLOW_SLACK_MS;
|
|
539
|
+
if (onRound && !walked)
|
|
540
|
+
r.walks.forEach((w, j) => (roundWalks[j] += w));
|
|
541
|
+
const last = current + Math.max(0, roundTime(roundWalks) - one);
|
|
542
|
+
const again = capped && onRound && !r.advanced && last > now;
|
|
543
|
+
if (capped && onRound && !r.advanced && !again)
|
|
544
|
+
rhythm.anchor = now + POLL_MS;
|
|
545
|
+
const next = again ? current : tickFrom(rhythm, now);
|
|
546
|
+
const nextAt = again ? last : next + after;
|
|
547
|
+
if (walked && entries.length > 8) {
|
|
548
|
+
const { starts } = pool(roundWalks);
|
|
549
|
+
r.walks.forEach((w, j) => {
|
|
550
|
+
if (w === 0)
|
|
551
|
+
return;
|
|
552
|
+
if (current + starts[j] + one >= now)
|
|
553
|
+
roundWalks[j] += w;
|
|
554
|
+
else
|
|
555
|
+
(nextWalks ??= entries.map(() => 0))[j] += w;
|
|
556
|
+
});
|
|
557
|
+
rhythm.anchor = current + roundTime(roundWalks) + POLL_MS;
|
|
558
|
+
}
|
|
559
|
+
if (walked && entries.length <= 8) {
|
|
560
|
+
const length = roundTime(r.walks);
|
|
561
|
+
tick = tickFrom(rhythm, now - one);
|
|
562
|
+
at = tick + Math.max(0, length - one);
|
|
563
|
+
rhythm.anchor = tick + length + POLL_MS;
|
|
564
|
+
nextWalks = r.walks;
|
|
565
|
+
}
|
|
566
|
+
else if (r.advanced ||
|
|
567
|
+
(r.waited && r.progressed) ||
|
|
568
|
+
(full && plan?.tick === undefined)) {
|
|
569
|
+
if (r.advanced && onRound && r.waited && !walked)
|
|
570
|
+
rhythm.anchor = now + one + POLL_MS;
|
|
571
|
+
const chunks = Math.ceil(entries.length / MAX_WAIT_IDS);
|
|
572
|
+
if (sent + chunks <= pollingSent(now)) {
|
|
573
|
+
tick = undefined;
|
|
574
|
+
at = now;
|
|
575
|
+
following = r.advanced;
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
tick = next;
|
|
579
|
+
at = nextAt;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
else if (full && plan?.tick !== undefined) {
|
|
583
|
+
tick = plan.tick;
|
|
584
|
+
at = Math.max(now, plan.tick + offset);
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
tick = next;
|
|
588
|
+
at = nextAt;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
if (at >= deadline) {
|
|
593
|
+
at = deadline;
|
|
594
|
+
tick = undefined;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
});
|
|
598
|
+
const readMulti = (chunk, reply) => {
|
|
599
|
+
const body = reply.json;
|
|
600
|
+
const list = body?.executions;
|
|
601
|
+
if (!Array.isArray(list) || list.length !== chunk.length)
|
|
602
|
+
return Effect.fail(new DecodeError({
|
|
603
|
+
what: "POST executions/wait",
|
|
604
|
+
message: "expected one record per id",
|
|
605
|
+
}));
|
|
606
|
+
return Effect.forEach(list, (item, i) => {
|
|
607
|
+
const missing = typeof item === "object" &&
|
|
608
|
+
item !== null &&
|
|
609
|
+
item.error !== undefined;
|
|
610
|
+
if (missing) {
|
|
611
|
+
const error = { error: item.error };
|
|
612
|
+
return Effect.succeed([chunk[i], apiError(404, error)]);
|
|
613
|
+
}
|
|
614
|
+
return decode(Job, "execution")(item).pipe(Effect.map((job) => [chunk[i], job]));
|
|
615
|
+
}).pipe(Effect.map((pairs) => ({
|
|
616
|
+
records: new Map(pairs),
|
|
617
|
+
waited: reply.waited && body?.waited === true,
|
|
618
|
+
})));
|
|
126
619
|
};
|
|
127
620
|
const start = (program, input) => request("POST", `programs/${segment(program)}/invoke`, {
|
|
128
621
|
...input,
|
|
@@ -137,6 +630,7 @@ export const make = (config) => {
|
|
|
137
630
|
programs: () => request("GET", "programs").pipe(Effect.flatMap(decode(Schema.Array(ProgramSummary), "programs"))),
|
|
138
631
|
inspect: (program) => request("GET", `programs/${segment(program)}`),
|
|
139
632
|
pause: (program, paused = true) => request("POST", `programs/${segment(program)}/pause`, { paused }),
|
|
633
|
+
delete: (program) => request("DELETE", `programs/${segment(program)}`).pipe(Effect.flatMap(decode(ProgramDeleted, "delete response"))),
|
|
140
634
|
executions: (program) => request("GET", `programs/${segment(program)}/executions`),
|
|
141
635
|
start,
|
|
142
636
|
execution,
|
|
@@ -202,6 +696,9 @@ export class FunctionsClient {
|
|
|
202
696
|
pause(program, paused = true, signal) {
|
|
203
697
|
return runPromise(this.client.pause(program, paused), signal);
|
|
204
698
|
}
|
|
699
|
+
delete(program, signal) {
|
|
700
|
+
return runPromise(this.client.delete(program), signal);
|
|
701
|
+
}
|
|
205
702
|
executions(program, signal) {
|
|
206
703
|
return runPromise(this.client.executions(program), signal);
|
|
207
704
|
}
|
package/dist/contracts.d.ts
CHANGED
|
@@ -277,6 +277,22 @@ export declare const ProgramSummary: Schema.Struct<{
|
|
|
277
277
|
readonly paused: Schema.Boolean;
|
|
278
278
|
}>;
|
|
279
279
|
export type ProgramSummary = typeof ProgramSummary.Type;
|
|
280
|
+
/** What `DELETE programs/{program}` removed (counts). */
|
|
281
|
+
export declare const ProgramDeleted: Schema.Struct<{
|
|
282
|
+
readonly deleted: Schema.Boolean;
|
|
283
|
+
readonly program: Schema.String;
|
|
284
|
+
readonly removed: Schema.Struct<{
|
|
285
|
+
readonly versions: Schema.Number;
|
|
286
|
+
readonly stateKeys: Schema.Number;
|
|
287
|
+
readonly subscriptions: Schema.Number;
|
|
288
|
+
readonly executions: Schema.Number;
|
|
289
|
+
readonly cancelledExecutions: Schema.Number;
|
|
290
|
+
readonly replays: Schema.Number;
|
|
291
|
+
readonly cancelledReplays: Schema.Number;
|
|
292
|
+
readonly streamEvents: Schema.Number;
|
|
293
|
+
}>;
|
|
294
|
+
}>;
|
|
295
|
+
export type ProgramDeleted = typeof ProgramDeleted.Type;
|
|
280
296
|
/** A schema that decodes without Effect services, so decoding is synchronous. */
|
|
281
297
|
export type Decodable = Schema.Top & {
|
|
282
298
|
readonly DecodingServices: never;
|
package/dist/contracts.js
CHANGED
|
@@ -111,6 +111,20 @@ export const ProgramSummary = Schema.Struct({
|
|
|
111
111
|
stateVersion: Schema.Number,
|
|
112
112
|
paused: Schema.Boolean,
|
|
113
113
|
});
|
|
114
|
+
export const ProgramDeleted = Schema.Struct({
|
|
115
|
+
deleted: Schema.Boolean,
|
|
116
|
+
program: Schema.String,
|
|
117
|
+
removed: Schema.Struct({
|
|
118
|
+
versions: Schema.Number,
|
|
119
|
+
stateKeys: Schema.Number,
|
|
120
|
+
subscriptions: Schema.Number,
|
|
121
|
+
executions: Schema.Number,
|
|
122
|
+
cancelledExecutions: Schema.Number,
|
|
123
|
+
replays: Schema.Number,
|
|
124
|
+
cancelledReplays: Schema.Number,
|
|
125
|
+
streamEvents: Schema.Number,
|
|
126
|
+
}),
|
|
127
|
+
});
|
|
114
128
|
export const NonNegativeInt = Schema.Number.check(Schema.isInt(), Schema.isGreaterThanOrEqualTo(0));
|
|
115
129
|
export const decodeWith = (schema, options, onError) => {
|
|
116
130
|
const parse = Schema.decodeUnknownResult(schema);
|
package/dist/errors.d.ts
CHANGED
|
@@ -20,8 +20,16 @@ declare const NetworkError_base: Schema.Class<NetworkError, Schema.TaggedStruct<
|
|
|
20
20
|
readonly url: Schema.String;
|
|
21
21
|
readonly message: Schema.String;
|
|
22
22
|
readonly cause: Schema.Unknown;
|
|
23
|
+
readonly reason: Schema.optional<Schema.String>;
|
|
23
24
|
}>, import("effect/Cause").YieldableError>;
|
|
24
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* The service could not be reached (DNS, TLS, connection reset, abort), or
|
|
27
|
+
* it ended a live stream. `reason` is set by the stream:
|
|
28
|
+
* - `functions_disabled`: Functions access was removed from the account;
|
|
29
|
+
* - `channel_closed`: the server closed the stream (subscribe again with
|
|
30
|
+
* `afterSeq`);
|
|
31
|
+
* - the server's reason of a refused join (e.g. `functions_unavailable`).
|
|
32
|
+
*/
|
|
25
33
|
export declare class NetworkError extends NetworkError_base {
|
|
26
34
|
}
|
|
27
35
|
declare const ApiError_base: Schema.Class<ApiError, Schema.TaggedStruct<"ApiError", {
|
package/dist/errors.js
CHANGED
|
@@ -13,7 +13,12 @@ export const Failure = Schema.Struct({
|
|
|
13
13
|
export const messageOf = (cause) => cause instanceof Error ? cause.message : String(cause);
|
|
14
14
|
export class ConfigError extends Schema.TaggedError()("ConfigError", { message: Schema.String }) {
|
|
15
15
|
}
|
|
16
|
-
export class NetworkError extends Schema.TaggedError()("NetworkError", {
|
|
16
|
+
export class NetworkError extends Schema.TaggedError()("NetworkError", {
|
|
17
|
+
url: Schema.String,
|
|
18
|
+
message: Schema.String,
|
|
19
|
+
cause: Schema.Unknown,
|
|
20
|
+
reason: Schema.optional(Schema.String),
|
|
21
|
+
}) {
|
|
17
22
|
}
|
|
18
23
|
export class ApiError extends Schema.TaggedError()("ApiError", {
|
|
19
24
|
status: Schema.Number,
|