@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/stream.js
CHANGED
|
@@ -6,19 +6,33 @@ export { ExecutionEvent, Gap } from "./contracts.js";
|
|
|
6
6
|
const decoder = (schema, what) => decodeWith(schema, undefined, (message) => new DecodeError({ what, message }));
|
|
7
7
|
const decodeEvent = decoder(ExecutionEvent, "execution event");
|
|
8
8
|
const decodeGap = decoder(Gap, "stream gap");
|
|
9
|
+
const refusedKey = 4401;
|
|
10
|
+
const defaultKeyRefusalWindowMs = 360_000;
|
|
11
|
+
const reconnectDelay = (closes) => Math.min(5_000, 500 * 2 ** closes) * (0.5 + Math.random() / 2);
|
|
9
12
|
function connect(options, handlers) {
|
|
10
13
|
let cursor = options.afterSeq;
|
|
14
|
+
const window = options.keyRefusalWindowMs ?? defaultKeyRefusalWindowMs;
|
|
11
15
|
const endpoint = new URL("/v1/stream", options.url);
|
|
12
16
|
endpoint.protocol =
|
|
13
17
|
endpoint.protocol === "https:" || endpoint.protocol === "wss:"
|
|
14
18
|
? "wss:"
|
|
15
19
|
: "ws:";
|
|
16
20
|
const socket = new Socket(endpoint.toString(), {
|
|
17
|
-
params: { api_key: options.apiKey },
|
|
21
|
+
params: { api_key: options.apiKey, on_refusal: "close" },
|
|
18
22
|
transport: WebSocket,
|
|
19
23
|
});
|
|
20
24
|
const channel = socket.channel("functions", () => cursor === undefined ? {} : { after_seq: cursor });
|
|
21
|
-
|
|
25
|
+
let account;
|
|
26
|
+
let checked = false;
|
|
27
|
+
let held = [];
|
|
28
|
+
const gated = (handle) => (raw) => {
|
|
29
|
+
if (closed)
|
|
30
|
+
return;
|
|
31
|
+
if (!checked)
|
|
32
|
+
return void held.push(() => handle(raw));
|
|
33
|
+
handle(raw);
|
|
34
|
+
};
|
|
35
|
+
channel.on("execution", gated((raw) => {
|
|
22
36
|
const result = decodeEvent(raw);
|
|
23
37
|
if (Result.isFailure(result))
|
|
24
38
|
return handlers.fail(result.failure);
|
|
@@ -27,26 +41,110 @@ function connect(options, handlers) {
|
|
|
27
41
|
return;
|
|
28
42
|
cursor = event.seq;
|
|
29
43
|
handlers.message({ _tag: "Execution", event });
|
|
30
|
-
});
|
|
31
|
-
channel.on("gap", (raw) => {
|
|
44
|
+
}));
|
|
45
|
+
channel.on("gap", gated((raw) => {
|
|
32
46
|
const result = decodeGap(raw);
|
|
33
47
|
if (Result.isFailure(result))
|
|
34
48
|
return handlers.fail(result.failure);
|
|
35
49
|
cursor = result.success.resumeAfter;
|
|
36
50
|
handlers.message({ _tag: "Gap", gap: result.success });
|
|
51
|
+
}));
|
|
52
|
+
let closed = false;
|
|
53
|
+
let timer;
|
|
54
|
+
const stop = () => {
|
|
55
|
+
closed = true;
|
|
56
|
+
held = [];
|
|
57
|
+
clearTimeout(timer);
|
|
58
|
+
channel.leave();
|
|
59
|
+
socket.disconnect();
|
|
60
|
+
};
|
|
61
|
+
const fail = (message, cause, reason) => {
|
|
62
|
+
if (closed)
|
|
63
|
+
return;
|
|
64
|
+
stop();
|
|
65
|
+
handlers.fail(new NetworkError({
|
|
66
|
+
url: endpoint.toString(),
|
|
67
|
+
message,
|
|
68
|
+
cause,
|
|
69
|
+
...(reason === undefined ? {} : { reason }),
|
|
70
|
+
}));
|
|
71
|
+
};
|
|
72
|
+
channel.on("status", gated((raw) => {
|
|
73
|
+
const status = raw;
|
|
74
|
+
if (status?.available === false && status.reason === "functions_disabled")
|
|
75
|
+
fail("Functions access was removed from this account: the server closed the stream", raw, "functions_disabled");
|
|
76
|
+
}));
|
|
77
|
+
channel.onClose((cause) => fail(`The server closed the stream: subscribe again with afterSeq ${cursor ?? "(unset)"}`, cause, "channel_closed"));
|
|
78
|
+
let closes = 0;
|
|
79
|
+
let pending = false;
|
|
80
|
+
const reconnect = () => {
|
|
81
|
+
if (pending)
|
|
82
|
+
return;
|
|
83
|
+
pending = true;
|
|
84
|
+
socket.disconnect();
|
|
85
|
+
timer = setTimeout(() => {
|
|
86
|
+
pending = false;
|
|
87
|
+
socket.connect();
|
|
88
|
+
}, reconnectDelay(closes++));
|
|
89
|
+
};
|
|
90
|
+
let refusedSince;
|
|
91
|
+
const refused = (reason, message, cause) => {
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
refusedSince ??= now;
|
|
94
|
+
if (now - refusedSince >= window)
|
|
95
|
+
return fail(message, cause, reason);
|
|
96
|
+
reconnect();
|
|
97
|
+
};
|
|
98
|
+
socket.onClose((event) => {
|
|
99
|
+
if (closed || pending)
|
|
100
|
+
return;
|
|
101
|
+
if (event?.code === refusedKey)
|
|
102
|
+
return refused("invalid_key", `The API key is no longer valid (revoked, unknown, or its account is not active): the server refused it for ${Math.round(window / 1000)} s`, event);
|
|
103
|
+
if (event?.code === 1000)
|
|
104
|
+
reconnect();
|
|
105
|
+
});
|
|
106
|
+
const transient = (error) => queueMicrotask(() => {
|
|
107
|
+
if (!closed && refusedSince === undefined)
|
|
108
|
+
handlers.transient(error);
|
|
109
|
+
});
|
|
110
|
+
socket.onError(transient);
|
|
111
|
+
channel.onError((error) => {
|
|
112
|
+
checked = false;
|
|
113
|
+
transient(error);
|
|
37
114
|
});
|
|
38
|
-
socket.onError((error) => handlers.transient(error));
|
|
39
|
-
channel.onError((error) => handlers.transient(error));
|
|
40
115
|
socket.connect();
|
|
41
|
-
channel
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
116
|
+
channel
|
|
117
|
+
.join()
|
|
118
|
+
.receive("ok", (reply) => {
|
|
119
|
+
if (closed)
|
|
120
|
+
return;
|
|
121
|
+
const named = reply?.account;
|
|
122
|
+
if (typeof named === "string") {
|
|
123
|
+
account ??= named;
|
|
124
|
+
if (named !== account)
|
|
125
|
+
return fail("The API key now belongs to another account: the stream of the previous account ended (its events are not followed on the new one)", reply, "account_changed");
|
|
126
|
+
}
|
|
127
|
+
checked = true;
|
|
128
|
+
closes = 0;
|
|
129
|
+
refusedSince = undefined;
|
|
130
|
+
const waiting = held;
|
|
131
|
+
held = [];
|
|
132
|
+
for (const handle of waiting)
|
|
133
|
+
if (!closed)
|
|
134
|
+
handle();
|
|
135
|
+
})
|
|
136
|
+
.receive("error", (reason) => {
|
|
137
|
+
if (closed)
|
|
138
|
+
return;
|
|
139
|
+
const code = reason?.reason;
|
|
140
|
+
const message = `Stream join refused: ${JSON.stringify(reason)}`;
|
|
141
|
+
if (code === "unauthorized")
|
|
142
|
+
return refused(code, message, reason);
|
|
143
|
+
fail(message, reason, typeof code === "string" ? code : undefined);
|
|
144
|
+
});
|
|
46
145
|
return {
|
|
47
146
|
close() {
|
|
48
|
-
|
|
49
|
-
socket.disconnect();
|
|
147
|
+
stop();
|
|
50
148
|
},
|
|
51
149
|
get cursor() {
|
|
52
150
|
return cursor;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johannc-dev/zyrk",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "SDK and CLI for Zyrk hosted functions
|
|
3
|
+
"version": "0.2.2",
|
|
4
|
+
"description": "SDK and CLI for Zyrk hosted functions",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"scripts": {
|
|
40
40
|
"build": "node scripts/build.mjs",
|
|
41
41
|
"typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
|
|
42
|
-
"test": "npm run build && npm run typecheck && node --test test/*.test.ts",
|
|
42
|
+
"test": "npm run build && npm run typecheck && node --test --test-timeout=60000 --test-force-exit test/*.test.ts",
|
|
43
43
|
"format": "prettier --write src types scripts test examples package.json tsconfig.json tsconfig.test.json README.md",
|
|
44
44
|
"format:check": "prettier --check src types scripts test examples package.json tsconfig.json tsconfig.test.json README.md",
|
|
45
45
|
"prepack": "npm run build"
|