@runku/client 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/README.md +139 -0
- package/dist/index.d.ts +162 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1022 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1022 @@
|
|
|
1
|
+
const MAX_ENVELOPE_BYTES = 2 * 1024 * 1024;
|
|
2
|
+
const MAX_DEPTH = 64;
|
|
3
|
+
const MAX_CONTAINER_ITEMS = 10_000;
|
|
4
|
+
const CROCKFORD = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
5
|
+
const BASE64URL = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
|
|
6
|
+
const textEncoder = new TextEncoder();
|
|
7
|
+
const ULID_PATTERN = "[0-7][0-9A-HJKMNP-TV-Z]{25}";
|
|
8
|
+
export class RunkuTimestamp {
|
|
9
|
+
micros;
|
|
10
|
+
constructor(micros) {
|
|
11
|
+
ensureI64(micros, "timestamp");
|
|
12
|
+
this.micros = micros;
|
|
13
|
+
Object.freeze(this);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class RunkuId {
|
|
17
|
+
value;
|
|
18
|
+
constructor(value) {
|
|
19
|
+
if (!new RegExp(`^[a-z0-9]{1,16}_${ULID_PATTERN}$`).test(value)) {
|
|
20
|
+
throw new TypeError("Runku typed ID is not canonical");
|
|
21
|
+
}
|
|
22
|
+
this.value = value;
|
|
23
|
+
Object.freeze(this);
|
|
24
|
+
}
|
|
25
|
+
toString() {
|
|
26
|
+
return this.value;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Validates a wire document ID and associates it with its expected table at compile time. */
|
|
30
|
+
export function documentId(tableName, value) {
|
|
31
|
+
if (!/^[a-z][A-Za-z0-9_]{0,63}$/.test(tableName)) {
|
|
32
|
+
throw new TypeError("Runku table name is invalid");
|
|
33
|
+
}
|
|
34
|
+
const id = new RunkuId(value);
|
|
35
|
+
if (!id.value.startsWith("doc_")) {
|
|
36
|
+
throw new TypeError("Runku document ID is not canonical");
|
|
37
|
+
}
|
|
38
|
+
return id;
|
|
39
|
+
}
|
|
40
|
+
/** Returns a zero-cost typed view using the registry emitted by `runku build`. */
|
|
41
|
+
export function typedClient(client) {
|
|
42
|
+
return client;
|
|
43
|
+
}
|
|
44
|
+
export class RunkuError extends Error {
|
|
45
|
+
code;
|
|
46
|
+
retryable;
|
|
47
|
+
status;
|
|
48
|
+
requestId;
|
|
49
|
+
constructor(input) {
|
|
50
|
+
super(input.message);
|
|
51
|
+
this.name = "RunkuError";
|
|
52
|
+
this.code = input.code;
|
|
53
|
+
this.retryable = input.retryable;
|
|
54
|
+
this.status = input.status;
|
|
55
|
+
this.requestId = input.requestId;
|
|
56
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
57
|
+
}
|
|
58
|
+
toString() {
|
|
59
|
+
return `${this.name}: ${this.code}${this.requestId === null ? "" : ` (${this.requestId})`}`;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export class RunkuClient {
|
|
63
|
+
#baseUrl;
|
|
64
|
+
#target;
|
|
65
|
+
#applicationKey;
|
|
66
|
+
#getBearer;
|
|
67
|
+
#timeoutMs;
|
|
68
|
+
#maxAttempts;
|
|
69
|
+
#retryDelayMs;
|
|
70
|
+
#fetch;
|
|
71
|
+
#webSocketFactory;
|
|
72
|
+
constructor(config) {
|
|
73
|
+
this.#baseUrl = validateBaseUrl(config.baseUrl);
|
|
74
|
+
this.#target = validateTarget(config.target);
|
|
75
|
+
this.#applicationKey = validateApplicationKey(config.applicationKey);
|
|
76
|
+
this.#getBearer = config.getBearer;
|
|
77
|
+
this.#timeoutMs = boundedInteger(config.timeoutMs ?? 30_000, 1, 300_000, "timeoutMs");
|
|
78
|
+
this.#maxAttempts = boundedInteger(config.maxAttempts ?? 2, 1, 5, "maxAttempts");
|
|
79
|
+
this.#retryDelayMs = boundedInteger(config.retryDelayMs ?? 50, 0, 10_000, "retryDelayMs");
|
|
80
|
+
const fetchImplementation = config.fetch ?? globalThis.fetch;
|
|
81
|
+
this.#fetch = fetchImplementation?.bind(globalThis);
|
|
82
|
+
this.#webSocketFactory = config.webSocketFactory;
|
|
83
|
+
if (typeof this.#fetch !== "function")
|
|
84
|
+
throw new TypeError("Fetch API is unavailable");
|
|
85
|
+
}
|
|
86
|
+
realtime(options = {}) {
|
|
87
|
+
return new RunkuRealtimeClient({
|
|
88
|
+
baseUrl: this.#baseUrl,
|
|
89
|
+
target: this.#target,
|
|
90
|
+
applicationKey: this.#applicationKey,
|
|
91
|
+
...(this.#getBearer === undefined ? {} : { getBearer: this.#getBearer }),
|
|
92
|
+
...(this.#webSocketFactory === undefined ? {} : { webSocketFactory: this.#webSocketFactory }),
|
|
93
|
+
...options,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
async query(functionName, argumentsValue, options = {}) {
|
|
97
|
+
return this.#call("query", functionName, argumentsValue, options, undefined);
|
|
98
|
+
}
|
|
99
|
+
async mutation(functionName, argumentsValue, options = {}) {
|
|
100
|
+
const operationId = options.operationId === undefined
|
|
101
|
+
? generateOperationId()
|
|
102
|
+
: validateOperationId(options.operationId);
|
|
103
|
+
return this.#call("mutation", functionName, argumentsValue, options, operationId);
|
|
104
|
+
}
|
|
105
|
+
async action(functionName, argumentsValue, options = {}) {
|
|
106
|
+
return this.#call("action", functionName, argumentsValue, options, undefined);
|
|
107
|
+
}
|
|
108
|
+
async #call(kind, functionName, argumentsValue, options, operationId) {
|
|
109
|
+
const target = options.target === undefined ? this.#target : validateTarget(options.target);
|
|
110
|
+
const envelope = {
|
|
111
|
+
version: 1,
|
|
112
|
+
target,
|
|
113
|
+
function: validateFunctionName(functionName),
|
|
114
|
+
arguments: encodeValue(argumentsValue),
|
|
115
|
+
};
|
|
116
|
+
if (operationId !== undefined)
|
|
117
|
+
envelope.operationId = operationId;
|
|
118
|
+
const body = JSON.stringify(envelope);
|
|
119
|
+
if (textEncoder.encode(body).byteLength > MAX_ENVELOPE_BYTES) {
|
|
120
|
+
throw localError("SDK_REQUEST_LIMIT_EXCEEDED", "The request exceeds the client limit.");
|
|
121
|
+
}
|
|
122
|
+
const attempts = kind === "action" ? 1 : this.#maxAttempts;
|
|
123
|
+
const lifecycle = abortLifecycle(options.signal, this.#timeoutMs);
|
|
124
|
+
try {
|
|
125
|
+
let lastError;
|
|
126
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
127
|
+
try {
|
|
128
|
+
let bearer;
|
|
129
|
+
try {
|
|
130
|
+
const resolved = await this.#getBearer?.();
|
|
131
|
+
bearer = resolved === null || resolved === undefined
|
|
132
|
+
? undefined
|
|
133
|
+
: validateOptionalCredential(resolved, 16 * 1024, "bearer");
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
throw localError("SDK_CREDENTIAL_INVALID", "The bearer credential could not be resolved.");
|
|
137
|
+
}
|
|
138
|
+
const headers = new Headers({ accept: "application/json", "content-type": "application/json" });
|
|
139
|
+
headers.set("x-runku-key", this.#applicationKey);
|
|
140
|
+
if (bearer !== undefined)
|
|
141
|
+
headers.set("authorization", `Bearer ${bearer}`);
|
|
142
|
+
const response = await this.#fetch(`${this.#baseUrl}/v1/${kind}`, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
headers,
|
|
145
|
+
body,
|
|
146
|
+
signal: lifecycle.signal,
|
|
147
|
+
});
|
|
148
|
+
validateContentType(response);
|
|
149
|
+
const bytes = await readBounded(response);
|
|
150
|
+
const decoded = decodeJson(bytes);
|
|
151
|
+
const headerRequestId = response.headers.get("x-runku-request-id");
|
|
152
|
+
if (response.status === 200)
|
|
153
|
+
return decodeSuccess(decoded, kind, headerRequestId);
|
|
154
|
+
const error = decodeFailure(decoded, response.status, headerRequestId);
|
|
155
|
+
if (!error.retryable || attempt === attempts)
|
|
156
|
+
throw error;
|
|
157
|
+
lastError = error;
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
if (lifecycle.signal.aborted)
|
|
161
|
+
throw abortError(lifecycle.timedOut());
|
|
162
|
+
const normalized = error instanceof RunkuError
|
|
163
|
+
? error
|
|
164
|
+
: localError("SDK_NETWORK_ERROR", "The network request failed.", true);
|
|
165
|
+
if (!normalized.retryable || attempt === attempts)
|
|
166
|
+
throw normalized;
|
|
167
|
+
lastError = normalized;
|
|
168
|
+
}
|
|
169
|
+
try {
|
|
170
|
+
await delay(this.#retryDelayMs * attempt, lifecycle.signal);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
throw abortError(lifecycle.timedOut());
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
throw lastError ?? localError("SDK_INTERNAL_ERROR", "The client request failed.");
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
lifecycle.dispose();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
export class RunkuRealtimeClient {
|
|
184
|
+
#baseUrl;
|
|
185
|
+
#target;
|
|
186
|
+
#applicationKey;
|
|
187
|
+
#getBearer;
|
|
188
|
+
#factory;
|
|
189
|
+
#initialDelayMs;
|
|
190
|
+
#maximumDelayMs;
|
|
191
|
+
#subscriptions = new Map();
|
|
192
|
+
#byServerId = new Map();
|
|
193
|
+
#pending = new Map();
|
|
194
|
+
#socket = null;
|
|
195
|
+
#connectPromise = null;
|
|
196
|
+
#resolveAuthentication = null;
|
|
197
|
+
#rejectAuthentication = null;
|
|
198
|
+
#authenticationRequestId = null;
|
|
199
|
+
#closed = false;
|
|
200
|
+
#reconnectAttempt = 0;
|
|
201
|
+
#reconnectTimer = null;
|
|
202
|
+
constructor(config) {
|
|
203
|
+
this.#baseUrl = validateBaseUrl(config.baseUrl);
|
|
204
|
+
this.#target = validateTarget(config.target);
|
|
205
|
+
this.#applicationKey = validateApplicationKey(config.applicationKey);
|
|
206
|
+
this.#getBearer = config.getBearer;
|
|
207
|
+
this.#initialDelayMs = boundedInteger(config.reconnectInitialDelayMs ?? 100, 0, 60_000, "reconnectInitialDelayMs");
|
|
208
|
+
this.#maximumDelayMs = boundedInteger(config.reconnectMaximumDelayMs ?? 10_000, 1, 300_000, "reconnectMaximumDelayMs");
|
|
209
|
+
if (this.#initialDelayMs > this.#maximumDelayMs)
|
|
210
|
+
throw new TypeError("reconnect delays are inverted");
|
|
211
|
+
this.#factory = config.webSocketFactory ?? defaultWebSocketFactory;
|
|
212
|
+
}
|
|
213
|
+
subscribe(functionName, argumentsValue, options) {
|
|
214
|
+
if (this.#closed)
|
|
215
|
+
throw localError("SDK_REALTIME_CLOSED", "The Realtime client is closed.");
|
|
216
|
+
const localId = generateResourceId("req");
|
|
217
|
+
let resolveReady = () => undefined;
|
|
218
|
+
let rejectReady = () => undefined;
|
|
219
|
+
const ready = new Promise((resolve, reject) => {
|
|
220
|
+
resolveReady = resolve;
|
|
221
|
+
rejectReady = reject;
|
|
222
|
+
});
|
|
223
|
+
const record = {
|
|
224
|
+
localId,
|
|
225
|
+
functionName: validateFunctionName(functionName),
|
|
226
|
+
argumentsWire: encodeValue(argumentsValue),
|
|
227
|
+
target: options.target === undefined ? this.#target : validateTarget(options.target),
|
|
228
|
+
onValue: (state) => options.onValue(state),
|
|
229
|
+
onError: options.onError ?? (() => undefined),
|
|
230
|
+
resolveReady: (state) => resolveReady(state),
|
|
231
|
+
rejectReady,
|
|
232
|
+
serverId: null,
|
|
233
|
+
pendingRequestId: null,
|
|
234
|
+
active: true,
|
|
235
|
+
readySettled: false,
|
|
236
|
+
};
|
|
237
|
+
this.#subscriptions.set(localId, record);
|
|
238
|
+
if (options.signal?.aborted === true)
|
|
239
|
+
void this.#unsubscribe(record);
|
|
240
|
+
else
|
|
241
|
+
options.signal?.addEventListener("abort", () => { void this.#unsubscribe(record); }, { once: true });
|
|
242
|
+
void this.#ensureConnected().then(() => this.#sendSubscribe(record), (error) => this.#report(record, normalizeRealtimeError(error)));
|
|
243
|
+
const owner = this;
|
|
244
|
+
return {
|
|
245
|
+
ready,
|
|
246
|
+
get subscriptionId() { return record.serverId; },
|
|
247
|
+
unsubscribe: () => owner.#unsubscribe(record),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
close() {
|
|
251
|
+
if (this.#closed)
|
|
252
|
+
return;
|
|
253
|
+
this.#closed = true;
|
|
254
|
+
if (this.#reconnectTimer !== null)
|
|
255
|
+
clearTimeout(this.#reconnectTimer);
|
|
256
|
+
this.#reconnectTimer = null;
|
|
257
|
+
const error = localError("SDK_REALTIME_CLOSED", "The Realtime client is closed.");
|
|
258
|
+
for (const record of this.#subscriptions.values()) {
|
|
259
|
+
record.active = false;
|
|
260
|
+
if (!record.readySettled)
|
|
261
|
+
record.rejectReady(error);
|
|
262
|
+
}
|
|
263
|
+
this.#subscriptions.clear();
|
|
264
|
+
this.#pending.clear();
|
|
265
|
+
this.#byServerId.clear();
|
|
266
|
+
this.#socket?.close(1000, "client closed");
|
|
267
|
+
this.#socket = null;
|
|
268
|
+
}
|
|
269
|
+
async #ensureConnected() {
|
|
270
|
+
if (this.#closed)
|
|
271
|
+
throw localError("SDK_REALTIME_CLOSED", "The Realtime client is closed.");
|
|
272
|
+
if (this.#socket?.readyState === 1 && this.#authenticationRequestId === null)
|
|
273
|
+
return;
|
|
274
|
+
if (this.#connectPromise !== null)
|
|
275
|
+
return this.#connectPromise;
|
|
276
|
+
this.#connectPromise = this.#connect();
|
|
277
|
+
try {
|
|
278
|
+
await this.#connectPromise;
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
this.#connectPromise = null;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
async #connect() {
|
|
285
|
+
const socket = this.#factory(realtimeUrl(this.#baseUrl), ["runku.realtime.v1"]);
|
|
286
|
+
this.#socket = socket;
|
|
287
|
+
socket.binaryType = "arraybuffer";
|
|
288
|
+
socket.onmessage = (event) => this.#onMessage(event.data);
|
|
289
|
+
socket.onclose = () => this.#onClose();
|
|
290
|
+
socket.onerror = () => undefined;
|
|
291
|
+
await new Promise((resolve, reject) => {
|
|
292
|
+
socket.onopen = () => resolve();
|
|
293
|
+
const fail = () => reject(localError("SDK_REALTIME_NETWORK_ERROR", "The Realtime connection failed.", true));
|
|
294
|
+
const original = socket.onclose;
|
|
295
|
+
socket.onclose = (event) => { original?.(event); fail(); };
|
|
296
|
+
});
|
|
297
|
+
let bearer;
|
|
298
|
+
try {
|
|
299
|
+
const resolved = await this.#getBearer?.();
|
|
300
|
+
bearer = resolved === null || resolved === undefined
|
|
301
|
+
? undefined
|
|
302
|
+
: validateOptionalCredential(resolved, 16 * 1024, "bearer");
|
|
303
|
+
}
|
|
304
|
+
catch {
|
|
305
|
+
socket.close(1008, "credential unavailable");
|
|
306
|
+
throw localError("SDK_CREDENTIAL_INVALID", "The bearer credential could not be resolved.");
|
|
307
|
+
}
|
|
308
|
+
const requestId = generateResourceId("req");
|
|
309
|
+
this.#authenticationRequestId = requestId;
|
|
310
|
+
const authenticated = new Promise((resolve, reject) => {
|
|
311
|
+
this.#resolveAuthentication = resolve;
|
|
312
|
+
this.#rejectAuthentication = reject;
|
|
313
|
+
});
|
|
314
|
+
socket.send(JSON.stringify({
|
|
315
|
+
type: "authenticate",
|
|
316
|
+
version: 1,
|
|
317
|
+
requestId,
|
|
318
|
+
applicationKey: this.#applicationKey,
|
|
319
|
+
bearer: bearer ?? null,
|
|
320
|
+
}));
|
|
321
|
+
await authenticated;
|
|
322
|
+
this.#reconnectAttempt = 0;
|
|
323
|
+
}
|
|
324
|
+
#onMessage(data) {
|
|
325
|
+
if (typeof data !== "string" || textEncoder.encode(data).byteLength > 64 * 1024) {
|
|
326
|
+
this.#socket?.close(1008, "invalid message");
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
let message;
|
|
330
|
+
try {
|
|
331
|
+
message = decodeRealtimeMessage(JSON.parse(data));
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
this.#socket?.close(1008, "invalid message");
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
if (message.type === "authentication_accepted") {
|
|
338
|
+
if (message.requestId !== this.#authenticationRequestId) {
|
|
339
|
+
this.#socket?.close(1008, "invalid auth correlation");
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
this.#authenticationRequestId = null;
|
|
343
|
+
this.#resolveAuthentication?.();
|
|
344
|
+
this.#resolveAuthentication = null;
|
|
345
|
+
this.#rejectAuthentication = null;
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
if (message.type === "state") {
|
|
349
|
+
const record = (message.requestId === null ? this.#byServerId.get(message.subscriptionId) : this.#pending.get(message.requestId));
|
|
350
|
+
if (record === undefined || !record.active)
|
|
351
|
+
return;
|
|
352
|
+
if (message.requestId !== null) {
|
|
353
|
+
this.#pending.delete(message.requestId);
|
|
354
|
+
record.pendingRequestId = null;
|
|
355
|
+
if (record.serverId !== null)
|
|
356
|
+
this.#byServerId.delete(record.serverId);
|
|
357
|
+
record.serverId = message.subscriptionId;
|
|
358
|
+
this.#byServerId.set(message.subscriptionId, record);
|
|
359
|
+
}
|
|
360
|
+
const state = message.state;
|
|
361
|
+
record.onValue(state);
|
|
362
|
+
if (!record.readySettled) {
|
|
363
|
+
record.readySettled = true;
|
|
364
|
+
record.resolveReady(state);
|
|
365
|
+
}
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
if (message.type === "resync_required") {
|
|
369
|
+
const record = this.#byServerId.get(message.subscriptionId);
|
|
370
|
+
if (record !== undefined && record.active) {
|
|
371
|
+
this.#byServerId.delete(message.subscriptionId);
|
|
372
|
+
record.serverId = null;
|
|
373
|
+
this.#sendSubscribe(record);
|
|
374
|
+
}
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
if (message.type === "error") {
|
|
378
|
+
const record = message.requestId === null
|
|
379
|
+
? (message.subscriptionId === null ? undefined : this.#byServerId.get(message.subscriptionId))
|
|
380
|
+
: this.#pending.get(message.requestId);
|
|
381
|
+
const error = localError(message.code, "The Realtime operation failed.", message.retryable);
|
|
382
|
+
if (record !== undefined) {
|
|
383
|
+
if (message.requestId !== null) {
|
|
384
|
+
this.#pending.delete(message.requestId);
|
|
385
|
+
record.pendingRequestId = null;
|
|
386
|
+
}
|
|
387
|
+
this.#report(record, error);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
#sendSubscribe(record) {
|
|
392
|
+
if (!record.active || record.pendingRequestId !== null || this.#socket?.readyState !== 1)
|
|
393
|
+
return;
|
|
394
|
+
const requestId = generateResourceId("req");
|
|
395
|
+
record.pendingRequestId = requestId;
|
|
396
|
+
this.#pending.set(requestId, record);
|
|
397
|
+
this.#socket.send(JSON.stringify({
|
|
398
|
+
type: "subscribe",
|
|
399
|
+
version: 1,
|
|
400
|
+
requestId,
|
|
401
|
+
target: record.target,
|
|
402
|
+
function: record.functionName,
|
|
403
|
+
arguments: record.argumentsWire,
|
|
404
|
+
}));
|
|
405
|
+
}
|
|
406
|
+
async #unsubscribe(record) {
|
|
407
|
+
if (!record.active)
|
|
408
|
+
return;
|
|
409
|
+
record.active = false;
|
|
410
|
+
this.#subscriptions.delete(record.localId);
|
|
411
|
+
if (record.pendingRequestId !== null)
|
|
412
|
+
this.#pending.delete(record.pendingRequestId);
|
|
413
|
+
if (!record.readySettled) {
|
|
414
|
+
record.readySettled = true;
|
|
415
|
+
record.rejectReady(localError("SDK_ABORTED", "The subscription was cancelled."));
|
|
416
|
+
}
|
|
417
|
+
if (record.serverId !== null) {
|
|
418
|
+
const serverId = record.serverId;
|
|
419
|
+
record.serverId = null;
|
|
420
|
+
this.#byServerId.delete(serverId);
|
|
421
|
+
if (this.#socket?.readyState === 1) {
|
|
422
|
+
this.#socket.send(JSON.stringify({
|
|
423
|
+
type: "unsubscribe",
|
|
424
|
+
version: 1,
|
|
425
|
+
requestId: generateResourceId("req"),
|
|
426
|
+
subscriptionId: serverId,
|
|
427
|
+
}));
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
#report(record, error) {
|
|
432
|
+
record.onError(error);
|
|
433
|
+
if (!record.readySettled && !error.retryable) {
|
|
434
|
+
record.readySettled = true;
|
|
435
|
+
record.rejectReady(error);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
#onClose() {
|
|
439
|
+
const error = localError("SDK_REALTIME_DISCONNECTED", "The Realtime connection was interrupted.", true);
|
|
440
|
+
this.#rejectAuthentication?.(error);
|
|
441
|
+
this.#resolveAuthentication = null;
|
|
442
|
+
this.#rejectAuthentication = null;
|
|
443
|
+
this.#authenticationRequestId = null;
|
|
444
|
+
this.#socket = null;
|
|
445
|
+
this.#pending.clear();
|
|
446
|
+
this.#byServerId.clear();
|
|
447
|
+
for (const record of this.#subscriptions.values()) {
|
|
448
|
+
record.pendingRequestId = null;
|
|
449
|
+
record.serverId = null;
|
|
450
|
+
if (record.active)
|
|
451
|
+
record.onError(error);
|
|
452
|
+
}
|
|
453
|
+
this.#queueReconnect();
|
|
454
|
+
}
|
|
455
|
+
#queueReconnect() {
|
|
456
|
+
if (this.#closed || this.#reconnectTimer !== null
|
|
457
|
+
|| ![...this.#subscriptions.values()].some((record) => record.active))
|
|
458
|
+
return;
|
|
459
|
+
const delayMs = Math.min(this.#maximumDelayMs, this.#initialDelayMs * 2 ** Math.min(this.#reconnectAttempt, 16));
|
|
460
|
+
this.#reconnectAttempt += 1;
|
|
461
|
+
this.#reconnectTimer = setTimeout(() => {
|
|
462
|
+
this.#reconnectTimer = null;
|
|
463
|
+
void this.#ensureConnected().then(() => { for (const record of this.#subscriptions.values())
|
|
464
|
+
this.#sendSubscribe(record); }, () => this.#queueReconnect());
|
|
465
|
+
}, delayMs);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
function decodeRealtimeMessage(value) {
|
|
469
|
+
if (!isRecord(value) || value.version !== 1 || typeof value.type !== "string")
|
|
470
|
+
throw protocolError();
|
|
471
|
+
switch (value.type) {
|
|
472
|
+
case "authentication_accepted":
|
|
473
|
+
if (!hasExactKeys(value, ["type", "version", "requestId"]))
|
|
474
|
+
throw protocolError();
|
|
475
|
+
return { type: "authentication_accepted", requestId: parseResourceId(value.requestId, "req") };
|
|
476
|
+
case "state": {
|
|
477
|
+
if (!hasExactKeys(value, [
|
|
478
|
+
"type", "version", "requestId", "subscriptionId", "releaseId", "deliveryRevision",
|
|
479
|
+
"value", "resultHash", "snapshotSequence", "authorizedUntilMicros",
|
|
480
|
+
]))
|
|
481
|
+
throw protocolError();
|
|
482
|
+
const requestId = value.requestId === null ? null : parseResourceId(value.requestId, "req");
|
|
483
|
+
const subscriptionId = parseResourceId(value.subscriptionId, "sub");
|
|
484
|
+
const releaseId = parseResourceId(value.releaseId, "rel");
|
|
485
|
+
const deliveryRevision = parseU64(value.deliveryRevision);
|
|
486
|
+
if (deliveryRevision === 0n || typeof value.resultHash !== "string" || !/^[0-9a-f]{64}$/.test(value.resultHash)) {
|
|
487
|
+
throw protocolError();
|
|
488
|
+
}
|
|
489
|
+
const snapshotSequence = value.snapshotSequence === null ? null : parseU64(value.snapshotSequence);
|
|
490
|
+
const authorizedMicros = parseI64(value.authorizedUntilMicros, true);
|
|
491
|
+
if (authorizedMicros < 0n)
|
|
492
|
+
throw protocolError();
|
|
493
|
+
return {
|
|
494
|
+
type: "state",
|
|
495
|
+
requestId,
|
|
496
|
+
subscriptionId,
|
|
497
|
+
state: {
|
|
498
|
+
subscriptionId,
|
|
499
|
+
releaseId,
|
|
500
|
+
deliveryRevision,
|
|
501
|
+
value: decodeValue(value.value),
|
|
502
|
+
resultHash: value.resultHash,
|
|
503
|
+
snapshotSequence,
|
|
504
|
+
authorizedUntil: new RunkuTimestamp(authorizedMicros),
|
|
505
|
+
},
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
case "error": {
|
|
509
|
+
if (!hasExactKeys(value, [
|
|
510
|
+
"type", "version", "requestId", "subscriptionId", "deliveryRevision", "code", "retryable",
|
|
511
|
+
]))
|
|
512
|
+
throw protocolError();
|
|
513
|
+
if (typeof value.code !== "string" || !/^[A-Z][A-Z0-9_]{0,63}$/.test(value.code)
|
|
514
|
+
|| typeof value.retryable !== "boolean")
|
|
515
|
+
throw protocolError();
|
|
516
|
+
if (value.deliveryRevision !== null && parseU64(value.deliveryRevision) === 0n)
|
|
517
|
+
throw protocolError();
|
|
518
|
+
return {
|
|
519
|
+
type: "error",
|
|
520
|
+
requestId: value.requestId === null ? null : parseResourceId(value.requestId, "req"),
|
|
521
|
+
subscriptionId: value.subscriptionId === null ? null : parseResourceId(value.subscriptionId, "sub"),
|
|
522
|
+
code: value.code,
|
|
523
|
+
retryable: value.retryable,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
case "resync_required":
|
|
527
|
+
if (!hasExactKeys(value, ["type", "version", "subscriptionId", "code"])
|
|
528
|
+
|| typeof value.code !== "string" || !/^[A-Z][A-Z0-9_]{0,63}$/.test(value.code))
|
|
529
|
+
throw protocolError();
|
|
530
|
+
return { type: "resync_required", subscriptionId: parseResourceId(value.subscriptionId, "sub") };
|
|
531
|
+
case "unsubscribed":
|
|
532
|
+
if (!hasExactKeys(value, ["type", "version", "requestId", "subscriptionId"]))
|
|
533
|
+
throw protocolError();
|
|
534
|
+
parseResourceId(value.requestId, "req");
|
|
535
|
+
parseResourceId(value.subscriptionId, "sub");
|
|
536
|
+
return { type: "unsubscribed" };
|
|
537
|
+
case "pong":
|
|
538
|
+
if (!hasExactKeys(value, ["type", "version", "requestId"]))
|
|
539
|
+
throw protocolError();
|
|
540
|
+
parseResourceId(value.requestId, "req");
|
|
541
|
+
return { type: "pong" };
|
|
542
|
+
default:
|
|
543
|
+
throw protocolError();
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
function defaultWebSocketFactory(url, protocols) {
|
|
547
|
+
if (typeof globalThis.WebSocket !== "function") {
|
|
548
|
+
throw localError("SDK_REALTIME_UNAVAILABLE", "The WebSocket API is unavailable.");
|
|
549
|
+
}
|
|
550
|
+
return new globalThis.WebSocket(url, [...protocols]);
|
|
551
|
+
}
|
|
552
|
+
function realtimeUrl(baseUrl) {
|
|
553
|
+
const url = new URL(baseUrl);
|
|
554
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
555
|
+
url.pathname = "/v1/realtime";
|
|
556
|
+
return url.toString();
|
|
557
|
+
}
|
|
558
|
+
function normalizeRealtimeError(error) {
|
|
559
|
+
return error instanceof RunkuError
|
|
560
|
+
? error
|
|
561
|
+
: localError("SDK_REALTIME_NETWORK_ERROR", "The Realtime connection failed.", true);
|
|
562
|
+
}
|
|
563
|
+
function parseResourceId(value, prefix) {
|
|
564
|
+
if (typeof value !== "string" || !new RegExp(`^${prefix}_${ULID_PATTERN}$`).test(value))
|
|
565
|
+
throw protocolError();
|
|
566
|
+
return value;
|
|
567
|
+
}
|
|
568
|
+
export function encodeValue(value, depth = 0) {
|
|
569
|
+
if (depth > MAX_DEPTH)
|
|
570
|
+
throw new TypeError("Runku value exceeds depth limit");
|
|
571
|
+
if (value === null)
|
|
572
|
+
return { type: "null" };
|
|
573
|
+
if (typeof value === "boolean")
|
|
574
|
+
return { type: "boolean", value };
|
|
575
|
+
if (typeof value === "bigint") {
|
|
576
|
+
ensureI64(value, "int64");
|
|
577
|
+
return { type: "int64", value: value.toString() };
|
|
578
|
+
}
|
|
579
|
+
if (typeof value === "number") {
|
|
580
|
+
if (!Number.isFinite(value) || Object.is(value, -0))
|
|
581
|
+
throw new TypeError("Runku float must be finite and not negative zero");
|
|
582
|
+
const view = new DataView(new ArrayBuffer(8));
|
|
583
|
+
view.setFloat64(0, value, false);
|
|
584
|
+
return { type: "float64", value: view.getBigUint64(0, false).toString(16).padStart(16, "0") };
|
|
585
|
+
}
|
|
586
|
+
if (typeof value === "string") {
|
|
587
|
+
if (!isUnicodeScalarString(value))
|
|
588
|
+
throw new TypeError("Runku string is not valid Unicode");
|
|
589
|
+
return { type: "string", value };
|
|
590
|
+
}
|
|
591
|
+
if (value instanceof Uint8Array) {
|
|
592
|
+
if (value.byteLength > MAX_ENVELOPE_BYTES)
|
|
593
|
+
throw new TypeError("Runku bytes exceed value limit");
|
|
594
|
+
return { type: "bytes", value: encodeBase64Url(value) };
|
|
595
|
+
}
|
|
596
|
+
if (value instanceof RunkuTimestamp)
|
|
597
|
+
return { type: "timestamp", value: value.micros.toString() };
|
|
598
|
+
if (value instanceof RunkuId)
|
|
599
|
+
return { type: "typed_id", value: value.value };
|
|
600
|
+
if (Array.isArray(value)) {
|
|
601
|
+
if (value.length > MAX_CONTAINER_ITEMS)
|
|
602
|
+
throw new TypeError("Runku array exceeds item limit");
|
|
603
|
+
return { type: "array", value: value.map((item) => encodeValue(item, depth + 1)) };
|
|
604
|
+
}
|
|
605
|
+
if (typeof value === "object") {
|
|
606
|
+
const objectValue = value;
|
|
607
|
+
const prototype = Object.getPrototypeOf(value);
|
|
608
|
+
if (prototype !== Object.prototype && prototype !== null)
|
|
609
|
+
throw new TypeError("Runku object must be plain");
|
|
610
|
+
const keys = Object.keys(value);
|
|
611
|
+
if (keys.length > MAX_CONTAINER_ITEMS)
|
|
612
|
+
throw new TypeError("Runku object exceeds item limit");
|
|
613
|
+
if (keys.some((key) => !isUnicodeScalarString(key)))
|
|
614
|
+
throw new TypeError("Runku object key is not valid Unicode");
|
|
615
|
+
keys.sort(compareUtf8);
|
|
616
|
+
return {
|
|
617
|
+
type: "object",
|
|
618
|
+
value: keys.map((key) => ({ key, value: encodeValue(objectValue[key], depth + 1) })),
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
throw new TypeError("Unsupported Runku value");
|
|
622
|
+
}
|
|
623
|
+
export function decodeValue(wire, depth = 0) {
|
|
624
|
+
if (depth > MAX_DEPTH || !isRecord(wire) || typeof wire.type !== "string") {
|
|
625
|
+
throw protocolError();
|
|
626
|
+
}
|
|
627
|
+
const exact = (keys) => {
|
|
628
|
+
const actual = Object.keys(wire).sort();
|
|
629
|
+
if (actual.length !== keys.length || actual.some((key, index) => key !== [...keys].sort()[index])) {
|
|
630
|
+
throw protocolError();
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
switch (wire.type) {
|
|
634
|
+
case "null":
|
|
635
|
+
exact(["type"]);
|
|
636
|
+
return null;
|
|
637
|
+
case "boolean":
|
|
638
|
+
exact(["type", "value"]);
|
|
639
|
+
if (typeof wire.value !== "boolean")
|
|
640
|
+
throw protocolError();
|
|
641
|
+
return wire.value;
|
|
642
|
+
case "int64":
|
|
643
|
+
exact(["type", "value"]);
|
|
644
|
+
return parseI64(wire.value, false);
|
|
645
|
+
case "float64": {
|
|
646
|
+
exact(["type", "value"]);
|
|
647
|
+
if (typeof wire.value !== "string" || !/^[0-9a-f]{16}$/.test(wire.value))
|
|
648
|
+
throw protocolError();
|
|
649
|
+
const view = new DataView(new ArrayBuffer(8));
|
|
650
|
+
view.setBigUint64(0, BigInt(`0x${wire.value}`), false);
|
|
651
|
+
const value = view.getFloat64(0, false);
|
|
652
|
+
if (!Number.isFinite(value) || Object.is(value, -0))
|
|
653
|
+
throw protocolError();
|
|
654
|
+
return value;
|
|
655
|
+
}
|
|
656
|
+
case "string":
|
|
657
|
+
exact(["type", "value"]);
|
|
658
|
+
if (typeof wire.value !== "string" || !isUnicodeScalarString(wire.value))
|
|
659
|
+
throw protocolError();
|
|
660
|
+
return wire.value;
|
|
661
|
+
case "bytes":
|
|
662
|
+
exact(["type", "value"]);
|
|
663
|
+
if (typeof wire.value !== "string")
|
|
664
|
+
throw protocolError();
|
|
665
|
+
return decodeBase64Url(wire.value);
|
|
666
|
+
case "timestamp":
|
|
667
|
+
exact(["type", "value"]);
|
|
668
|
+
return new RunkuTimestamp(parseI64(wire.value, true));
|
|
669
|
+
case "typed_id":
|
|
670
|
+
exact(["type", "value"]);
|
|
671
|
+
if (typeof wire.value !== "string")
|
|
672
|
+
throw protocolError();
|
|
673
|
+
return new RunkuId(wire.value);
|
|
674
|
+
case "array": {
|
|
675
|
+
exact(["type", "value"]);
|
|
676
|
+
if (!Array.isArray(wire.value) || wire.value.length > MAX_CONTAINER_ITEMS)
|
|
677
|
+
throw protocolError();
|
|
678
|
+
return wire.value.map((item) => decodeValue(item, depth + 1));
|
|
679
|
+
}
|
|
680
|
+
case "object": {
|
|
681
|
+
exact(["type", "value"]);
|
|
682
|
+
if (!Array.isArray(wire.value) || wire.value.length > MAX_CONTAINER_ITEMS)
|
|
683
|
+
throw protocolError();
|
|
684
|
+
const output = Object.create(null);
|
|
685
|
+
let previous;
|
|
686
|
+
for (const entry of wire.value) {
|
|
687
|
+
if (!isRecord(entry) || Object.keys(entry).sort().join(",") !== "key,value"
|
|
688
|
+
|| typeof entry.key !== "string" || !isUnicodeScalarString(entry.key))
|
|
689
|
+
throw protocolError();
|
|
690
|
+
if (previous !== undefined && compareUtf8(previous, entry.key) >= 0)
|
|
691
|
+
throw protocolError();
|
|
692
|
+
previous = entry.key;
|
|
693
|
+
output[entry.key] = decodeValue(entry.value, depth + 1);
|
|
694
|
+
}
|
|
695
|
+
return output;
|
|
696
|
+
}
|
|
697
|
+
default: throw protocolError();
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
function decodeSuccess(value, expectedKind, headerRequestId) {
|
|
701
|
+
if (!isRecord(value) || !hasExactKeys(value, ["version", "status", "requestId", "releaseId", "result", "metadata"])
|
|
702
|
+
|| value.version !== 1 || value.status !== "ok"
|
|
703
|
+
|| typeof value.requestId !== "string" || !new RegExp(`^req_${ULID_PATTERN}$`).test(value.requestId)
|
|
704
|
+
|| typeof value.releaseId !== "string" || !new RegExp(`^rel_${ULID_PATTERN}$`).test(value.releaseId)
|
|
705
|
+
|| (headerRequestId !== null && headerRequestId !== value.requestId))
|
|
706
|
+
throw protocolError();
|
|
707
|
+
return Object.freeze({
|
|
708
|
+
requestId: value.requestId,
|
|
709
|
+
releaseId: value.releaseId,
|
|
710
|
+
value: decodeValue(value.result),
|
|
711
|
+
metadata: decodeMetadata(value.metadata, expectedKind),
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
function decodeMetadata(value, expectedKind) {
|
|
715
|
+
if (!isRecord(value) || value.kind !== expectedKind)
|
|
716
|
+
throw protocolError();
|
|
717
|
+
switch (expectedKind) {
|
|
718
|
+
case "query":
|
|
719
|
+
if (!hasExactKeys(value, ["kind", "snapshotSequence"]))
|
|
720
|
+
throw protocolError();
|
|
721
|
+
return Object.freeze({
|
|
722
|
+
kind: "query",
|
|
723
|
+
snapshotSequence: value.snapshotSequence === null ? null : parseU64(value.snapshotSequence),
|
|
724
|
+
});
|
|
725
|
+
case "mutation":
|
|
726
|
+
if (!hasExactKeys(value, ["kind", "commitSequence", "replayed", "attempts"])
|
|
727
|
+
|| typeof value.replayed !== "boolean" || typeof value.attempts !== "number"
|
|
728
|
+
|| !Number.isInteger(value.attempts) || value.attempts < 1 || value.attempts > 255)
|
|
729
|
+
throw protocolError();
|
|
730
|
+
return Object.freeze({
|
|
731
|
+
kind: "mutation",
|
|
732
|
+
commitSequence: value.commitSequence === null ? null : parseU64(value.commitSequence),
|
|
733
|
+
replayed: value.replayed,
|
|
734
|
+
attempts: value.attempts,
|
|
735
|
+
});
|
|
736
|
+
case "action":
|
|
737
|
+
if (!hasExactKeys(value, ["kind", "schedulesCreated"]))
|
|
738
|
+
throw protocolError();
|
|
739
|
+
return Object.freeze({ kind: "action", schedulesCreated: parseU64(value.schedulesCreated) });
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
function decodeFailure(value, status, headerRequestId) {
|
|
743
|
+
if (!isRecord(value) || !hasExactKeys(value, ["version", "status", "requestId", "error"])
|
|
744
|
+
|| value.version !== 1 || value.status !== "error"
|
|
745
|
+
|| typeof value.requestId !== "string" || !new RegExp(`^req_${ULID_PATTERN}$`).test(value.requestId)
|
|
746
|
+
|| (headerRequestId !== null && headerRequestId !== value.requestId)
|
|
747
|
+
|| !isRecord(value.error) || !hasExactKeys(value.error, ["code", "message", "retryable"])
|
|
748
|
+
|| typeof value.error.code !== "string"
|
|
749
|
+
|| !/^[A-Z][A-Z0-9_]{0,63}$/.test(value.error.code)
|
|
750
|
+
|| typeof value.error.message !== "string" || value.error.message.length === 0
|
|
751
|
+
|| textEncoder.encode(value.error.message).byteLength > 128 || !isUnicodeScalarString(value.error.message)
|
|
752
|
+
|| /\p{Cc}/u.test(value.error.message)
|
|
753
|
+
|| typeof value.error.retryable !== "boolean" || status < 400 || status > 599)
|
|
754
|
+
throw protocolError();
|
|
755
|
+
return new RunkuError({
|
|
756
|
+
code: value.error.code,
|
|
757
|
+
message: value.error.message,
|
|
758
|
+
retryable: value.error.retryable,
|
|
759
|
+
status,
|
|
760
|
+
requestId: value.requestId,
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
async function readBounded(response) {
|
|
764
|
+
if (response.body === null)
|
|
765
|
+
throw protocolError();
|
|
766
|
+
const reader = response.body.getReader();
|
|
767
|
+
const chunks = [];
|
|
768
|
+
let length = 0;
|
|
769
|
+
for (;;) {
|
|
770
|
+
const next = await reader.read();
|
|
771
|
+
if (next.done)
|
|
772
|
+
break;
|
|
773
|
+
length += next.value.byteLength;
|
|
774
|
+
if (length > MAX_ENVELOPE_BYTES) {
|
|
775
|
+
await reader.cancel();
|
|
776
|
+
throw localError("SDK_RESPONSE_LIMIT_EXCEEDED", "The response exceeds the client limit.");
|
|
777
|
+
}
|
|
778
|
+
chunks.push(next.value);
|
|
779
|
+
}
|
|
780
|
+
const output = new Uint8Array(length);
|
|
781
|
+
let offset = 0;
|
|
782
|
+
for (const chunk of chunks) {
|
|
783
|
+
output.set(chunk, offset);
|
|
784
|
+
offset += chunk.byteLength;
|
|
785
|
+
}
|
|
786
|
+
return output;
|
|
787
|
+
}
|
|
788
|
+
function validateContentType(response) {
|
|
789
|
+
const contentType = response.headers.get("content-type");
|
|
790
|
+
if (contentType === null || contentType.split(";", 1)[0]?.trim().toLowerCase() !== "application/json") {
|
|
791
|
+
throw protocolError();
|
|
792
|
+
}
|
|
793
|
+
const length = response.headers.get("content-length");
|
|
794
|
+
if (length !== null) {
|
|
795
|
+
if (!/^(?:0|[1-9][0-9]*)$/.test(length))
|
|
796
|
+
throw protocolError();
|
|
797
|
+
if (BigInt(length) > BigInt(MAX_ENVELOPE_BYTES)) {
|
|
798
|
+
throw localError("SDK_RESPONSE_LIMIT_EXCEEDED", "The response exceeds the client limit.");
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
function decodeJson(bytes) {
|
|
803
|
+
if (bytes.byteLength === 0)
|
|
804
|
+
throw protocolError();
|
|
805
|
+
try {
|
|
806
|
+
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
807
|
+
}
|
|
808
|
+
catch {
|
|
809
|
+
throw protocolError();
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
function validateBaseUrl(input) {
|
|
813
|
+
let url;
|
|
814
|
+
try {
|
|
815
|
+
url = new URL(input);
|
|
816
|
+
}
|
|
817
|
+
catch {
|
|
818
|
+
throw new TypeError("baseUrl is invalid");
|
|
819
|
+
}
|
|
820
|
+
if ((url.protocol !== "https:" && url.protocol !== "http:") || url.username !== "" || url.password !== ""
|
|
821
|
+
|| url.search !== "" || url.hash !== "" || (url.pathname !== "/" && url.pathname !== "")) {
|
|
822
|
+
throw new TypeError("baseUrl must be an HTTP(S) origin");
|
|
823
|
+
}
|
|
824
|
+
if (url.protocol === "http:"
|
|
825
|
+
&& url.hostname !== "localhost" && url.hostname !== "127.0.0.1" && url.hostname !== "[::1]") {
|
|
826
|
+
throw new TypeError("plain HTTP is only allowed for loopback development");
|
|
827
|
+
}
|
|
828
|
+
return url.origin;
|
|
829
|
+
}
|
|
830
|
+
function validateTarget(input) {
|
|
831
|
+
const bytes = textEncoder.encode(input).byteLength;
|
|
832
|
+
const validRelease = new RegExp(`^release:rel_${ULID_PATTERN}$`).test(input);
|
|
833
|
+
const validChannel = /^channel:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(input) && bytes <= 71;
|
|
834
|
+
const workspace = input.startsWith("workspace:") ? input.slice(10) : "";
|
|
835
|
+
const validWorkspace = workspace.length > 0 && textEncoder.encode(workspace).byteLength <= 100
|
|
836
|
+
&& workspace.split("/").every((part) => /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(part));
|
|
837
|
+
if (!validRelease && !validChannel && !validWorkspace)
|
|
838
|
+
throw new TypeError("target is not canonical");
|
|
839
|
+
return input;
|
|
840
|
+
}
|
|
841
|
+
function validateFunctionName(value) {
|
|
842
|
+
if (textEncoder.encode(value).byteLength > 128 || !/^[A-Za-z][A-Za-z0-9_.\/-]*$/.test(value)) {
|
|
843
|
+
throw new TypeError("function name is not canonical");
|
|
844
|
+
}
|
|
845
|
+
return value;
|
|
846
|
+
}
|
|
847
|
+
function validateOperationId(value) {
|
|
848
|
+
if (!new RegExp(`^opn_${ULID_PATTERN}$`).test(value))
|
|
849
|
+
throw new TypeError("operationId is not canonical");
|
|
850
|
+
return value;
|
|
851
|
+
}
|
|
852
|
+
function generateOperationId() {
|
|
853
|
+
return generateResourceId("opn");
|
|
854
|
+
}
|
|
855
|
+
function generateResourceId(prefix) {
|
|
856
|
+
const random = new Uint8Array(10);
|
|
857
|
+
if (globalThis.crypto === undefined || typeof globalThis.crypto.getRandomValues !== "function") {
|
|
858
|
+
throw localError("SDK_CRYPTO_UNAVAILABLE", "Web Crypto is unavailable.");
|
|
859
|
+
}
|
|
860
|
+
globalThis.crypto.getRandomValues(random);
|
|
861
|
+
const timestamp = BigInt(Date.now());
|
|
862
|
+
if (timestamp < 0n || timestamp > 0xffffffffffffn)
|
|
863
|
+
throw new TypeError("clock cannot produce an operation ID");
|
|
864
|
+
let randomness = 0n;
|
|
865
|
+
for (const byte of random)
|
|
866
|
+
randomness = (randomness << 8n) | BigInt(byte);
|
|
867
|
+
let value = (timestamp << 80n) | randomness;
|
|
868
|
+
let encoded = "";
|
|
869
|
+
for (let index = 0; index < 26; index += 1) {
|
|
870
|
+
encoded = CROCKFORD[Number(value & 31n)] + encoded;
|
|
871
|
+
value >>= 5n;
|
|
872
|
+
}
|
|
873
|
+
return `${prefix}_${encoded}`;
|
|
874
|
+
}
|
|
875
|
+
function validateOptionalCredential(value, maximum, name) {
|
|
876
|
+
if (value === undefined)
|
|
877
|
+
return undefined;
|
|
878
|
+
if (value.length === 0 || textEncoder.encode(value).byteLength > maximum || /[^\x21-\x7e]/.test(value)) {
|
|
879
|
+
throw new TypeError(`${name} is invalid`);
|
|
880
|
+
}
|
|
881
|
+
return value;
|
|
882
|
+
}
|
|
883
|
+
function validateApplicationKey(value) {
|
|
884
|
+
const publishable = new RegExp(`^rk_pub_v1_${ULID_PATTERN}_[A-Za-z0-9_-]{21}[AQgw]$`);
|
|
885
|
+
const secret = new RegExp(`^rk_sec_v1_${ULID_PATTERN}\\.[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$`);
|
|
886
|
+
if (!publishable.test(value) && !secret.test(value))
|
|
887
|
+
throw new TypeError("application key is not canonical");
|
|
888
|
+
return value;
|
|
889
|
+
}
|
|
890
|
+
function boundedInteger(value, minimum, maximum, name) {
|
|
891
|
+
if (!Number.isInteger(value) || value < minimum || value > maximum)
|
|
892
|
+
throw new TypeError(`${name} is outside limits`);
|
|
893
|
+
return value;
|
|
894
|
+
}
|
|
895
|
+
function ensureI64(value, name) {
|
|
896
|
+
if (value < -(1n << 63n) || value > (1n << 63n) - 1n)
|
|
897
|
+
throw new TypeError(`${name} is outside i64`);
|
|
898
|
+
}
|
|
899
|
+
function parseI64(value, timestamp) {
|
|
900
|
+
if (typeof value !== "string" || !/^(?:0|-?[1-9][0-9]*)$/.test(value) || value === "-0")
|
|
901
|
+
throw protocolError();
|
|
902
|
+
const parsed = BigInt(value);
|
|
903
|
+
ensureI64(parsed, timestamp ? "timestamp" : "int64");
|
|
904
|
+
return parsed;
|
|
905
|
+
}
|
|
906
|
+
function parseU64(value) {
|
|
907
|
+
if (typeof value !== "string" || !/^(?:0|[1-9][0-9]*)$/.test(value))
|
|
908
|
+
throw protocolError();
|
|
909
|
+
const parsed = BigInt(value);
|
|
910
|
+
if (parsed > (1n << 64n) - 1n)
|
|
911
|
+
throw protocolError();
|
|
912
|
+
return parsed;
|
|
913
|
+
}
|
|
914
|
+
function compareUtf8(left, right) {
|
|
915
|
+
const a = textEncoder.encode(left);
|
|
916
|
+
const b = textEncoder.encode(right);
|
|
917
|
+
const length = Math.min(a.length, b.length);
|
|
918
|
+
for (let index = 0; index < length; index += 1) {
|
|
919
|
+
const difference = a[index] - b[index];
|
|
920
|
+
if (difference !== 0)
|
|
921
|
+
return difference;
|
|
922
|
+
}
|
|
923
|
+
return a.length - b.length;
|
|
924
|
+
}
|
|
925
|
+
function isUnicodeScalarString(value) {
|
|
926
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
927
|
+
const unit = value.charCodeAt(index);
|
|
928
|
+
if (unit >= 0xd800 && unit <= 0xdbff) {
|
|
929
|
+
const next = value.charCodeAt(index + 1);
|
|
930
|
+
if (next < 0xdc00 || next > 0xdfff)
|
|
931
|
+
return false;
|
|
932
|
+
index += 1;
|
|
933
|
+
}
|
|
934
|
+
else if (unit >= 0xdc00 && unit <= 0xdfff) {
|
|
935
|
+
return false;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return true;
|
|
939
|
+
}
|
|
940
|
+
function encodeBase64Url(bytes) {
|
|
941
|
+
let output = "";
|
|
942
|
+
for (let index = 0; index < bytes.length; index += 3) {
|
|
943
|
+
const a = bytes[index];
|
|
944
|
+
const hasB = index + 1 < bytes.length;
|
|
945
|
+
const hasC = index + 2 < bytes.length;
|
|
946
|
+
const b = hasB ? bytes[index + 1] : 0;
|
|
947
|
+
const c = hasC ? bytes[index + 2] : 0;
|
|
948
|
+
output += BASE64URL[a >> 2];
|
|
949
|
+
output += BASE64URL[((a & 3) << 4) | (b >> 4)];
|
|
950
|
+
if (hasB)
|
|
951
|
+
output += BASE64URL[((b & 15) << 2) | (c >> 6)];
|
|
952
|
+
if (hasC)
|
|
953
|
+
output += BASE64URL[c & 63];
|
|
954
|
+
}
|
|
955
|
+
return output;
|
|
956
|
+
}
|
|
957
|
+
function decodeBase64Url(value) {
|
|
958
|
+
if (!/^[A-Za-z0-9_-]*$/.test(value) || value.length % 4 === 1)
|
|
959
|
+
throw protocolError();
|
|
960
|
+
const output = [];
|
|
961
|
+
let bits = 0;
|
|
962
|
+
let count = 0;
|
|
963
|
+
for (const character of value) {
|
|
964
|
+
const index = BASE64URL.indexOf(character);
|
|
965
|
+
if (index < 0)
|
|
966
|
+
throw protocolError();
|
|
967
|
+
bits = (bits << 6) | index;
|
|
968
|
+
count += 6;
|
|
969
|
+
if (count >= 8) {
|
|
970
|
+
count -= 8;
|
|
971
|
+
output.push((bits >> count) & 255);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
if (count > 0 && (bits & ((1 << count) - 1)) !== 0)
|
|
975
|
+
throw protocolError();
|
|
976
|
+
const decoded = Uint8Array.from(output);
|
|
977
|
+
if (encodeBase64Url(decoded) !== value)
|
|
978
|
+
throw protocolError();
|
|
979
|
+
return decoded;
|
|
980
|
+
}
|
|
981
|
+
function abortLifecycle(parent, timeoutMs) {
|
|
982
|
+
const controller = new AbortController();
|
|
983
|
+
let timeoutWon = false;
|
|
984
|
+
const parentAbort = () => controller.abort(parent?.reason);
|
|
985
|
+
if (parent?.aborted === true)
|
|
986
|
+
parentAbort();
|
|
987
|
+
else
|
|
988
|
+
parent?.addEventListener("abort", parentAbort, { once: true });
|
|
989
|
+
const timeout = setTimeout(() => { timeoutWon = true; controller.abort(); }, timeoutMs);
|
|
990
|
+
return {
|
|
991
|
+
signal: controller.signal,
|
|
992
|
+
timedOut: () => timeoutWon,
|
|
993
|
+
dispose: () => { clearTimeout(timeout); parent?.removeEventListener("abort", parentAbort); },
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
function delay(milliseconds, signal) {
|
|
997
|
+
if (milliseconds === 0)
|
|
998
|
+
return Promise.resolve();
|
|
999
|
+
return new Promise((resolve, reject) => {
|
|
1000
|
+
const timeout = setTimeout(() => { signal.removeEventListener("abort", abort); resolve(); }, milliseconds);
|
|
1001
|
+
const abort = () => { clearTimeout(timeout); reject(abortError(false)); };
|
|
1002
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1003
|
+
});
|
|
1004
|
+
}
|
|
1005
|
+
function abortError(timedOut) {
|
|
1006
|
+
return localError(timedOut ? "SDK_TIMEOUT" : "SDK_ABORTED", timedOut ? "The client deadline elapsed." : "The request was aborted.");
|
|
1007
|
+
}
|
|
1008
|
+
function protocolError() {
|
|
1009
|
+
return localError("SDK_RESPONSE_INVALID", "The server response is invalid.");
|
|
1010
|
+
}
|
|
1011
|
+
function localError(code, message, retryable = false) {
|
|
1012
|
+
return new RunkuError({ code, message, retryable, status: 0, requestId: null });
|
|
1013
|
+
}
|
|
1014
|
+
function isRecord(value) {
|
|
1015
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1016
|
+
}
|
|
1017
|
+
function hasExactKeys(value, expected) {
|
|
1018
|
+
const actual = Object.keys(value).sort();
|
|
1019
|
+
const wanted = [...expected].sort();
|
|
1020
|
+
return actual.length === wanted.length && actual.every((key, index) => key === wanted[index]);
|
|
1021
|
+
}
|
|
1022
|
+
//# sourceMappingURL=index.js.map
|