@tinycloud/sdk-services 3.5.0 → 3.6.0-beta.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.
@@ -0,0 +1,831 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/context.ts
21
+ var context_exports = {};
22
+ __export(context_exports, {
23
+ ServiceContext: () => ServiceContext
24
+ });
25
+ module.exports = __toCommonJS(context_exports);
26
+
27
+ // src/types.ts
28
+ var ErrorCodes = {
29
+ // Common errors
30
+ NOT_FOUND: "NOT_FOUND",
31
+ AUTH_EXPIRED: "AUTH_EXPIRED",
32
+ AUTH_REQUIRED: "AUTH_REQUIRED",
33
+ AUTH_UNAUTHORIZED: "AUTH_UNAUTHORIZED",
34
+ /** HTTP 401: the delegation the invocation cites was revoked. */
35
+ AUTH_DELEGATION_REVOKED: "AUTH_DELEGATION_REVOKED",
36
+ /** HTTP 401: a delegation the cited one rests on was revoked. */
37
+ AUTH_DELEGATION_ANCESTOR_REVOKED: "AUTH_DELEGATION_ANCESTOR_REVOKED",
38
+ NETWORK_ERROR: "NETWORK_ERROR",
39
+ TIMEOUT: "TIMEOUT",
40
+ ABORTED: "ABORTED",
41
+ INVALID_INPUT: "INVALID_INPUT",
42
+ PERMISSION_DENIED: "PERMISSION_DENIED",
43
+ // Node retry signals (tinycloud-node #347). `meta.retryable` says whether a
44
+ // retry is safe; `meta.retryAfterMs` carries the node's Retry-After.
45
+ /** HTTP 503 `sql_busy:<stage>` / `unavailable_retryable`/`read_audit_busy`: nothing was applied; retry after `meta.retryAfterMs`. */
46
+ NODE_BUSY: "NODE_BUSY",
47
+ /** HTTP 503 `request_timeout`: the node's deadline passed before any write began; nothing was applied. */
48
+ REQUEST_TIMEOUT: "REQUEST_TIMEOUT",
49
+ /** HTTP 500 `outcome_unknown`: a write may have been applied; read back before retrying a non-idempotent operation. */
50
+ OUTCOME_UNKNOWN: "OUTCOME_UNKNOWN",
51
+ // KV-specific errors
52
+ KV_NOT_FOUND: "KV_NOT_FOUND",
53
+ KV_WRITE_FAILED: "KV_WRITE_FAILED",
54
+ KV_PRECONDITION_FAILED: "KV_PRECONDITION_FAILED",
55
+ KV_CONFLICT: "KV_CONFLICT",
56
+ KV_RESPONSE_TOO_LARGE: "KV_RESPONSE_TOO_LARGE",
57
+ /** HTTP 410 from the change feed: discard sync state and restart without a cursor; `meta.reason` says why. */
58
+ KV_SYNC_RESET_REQUIRED: "KV_SYNC_RESET_REQUIRED",
59
+ /** HTTP 403 from the change feed: the retention grant was refused; `meta.reason` says why. */
60
+ KV_RETENTION_GRANT_REFUSED: "KV_RETENTION_GRANT_REFUSED",
61
+ // SQL-specific errors
62
+ SQL_ERROR: "SQL_ERROR",
63
+ SQL_PERMISSION_DENIED: "SQL_PERMISSION_DENIED",
64
+ SQL_DATABASE_NOT_FOUND: "SQL_DATABASE_NOT_FOUND",
65
+ SQL_RESPONSE_TOO_LARGE: "SQL_RESPONSE_TOO_LARGE",
66
+ SQL_QUOTA_EXCEEDED: "SQL_QUOTA_EXCEEDED",
67
+ SQL_INVALID_STATEMENT: "SQL_INVALID_STATEMENT",
68
+ SQL_SCHEMA_ERROR: "SQL_SCHEMA_ERROR",
69
+ SQL_READONLY_VIOLATION: "SQL_READONLY_VIOLATION",
70
+ // Storage quota errors
71
+ STORAGE_QUOTA_EXCEEDED: "STORAGE_QUOTA_EXCEEDED",
72
+ STORAGE_LIMIT_REACHED: "STORAGE_LIMIT_REACHED",
73
+ /** The node answered the usage read without usage numbers (it predates the read). */
74
+ STORAGE_STATUS_UNAVAILABLE: "STORAGE_STATUS_UNAVAILABLE",
75
+ // DuckDB-specific errors
76
+ DUCKDB_ERROR: "DUCKDB_ERROR",
77
+ DUCKDB_PERMISSION_DENIED: "DUCKDB_PERMISSION_DENIED",
78
+ DUCKDB_DATABASE_NOT_FOUND: "DUCKDB_DATABASE_NOT_FOUND",
79
+ DUCKDB_RESPONSE_TOO_LARGE: "DUCKDB_RESPONSE_TOO_LARGE",
80
+ DUCKDB_QUOTA_EXCEEDED: "DUCKDB_QUOTA_EXCEEDED",
81
+ DUCKDB_INVALID_STATEMENT: "DUCKDB_INVALID_STATEMENT",
82
+ DUCKDB_SCHEMA_ERROR: "DUCKDB_SCHEMA_ERROR",
83
+ DUCKDB_READONLY_VIOLATION: "DUCKDB_READONLY_VIOLATION"
84
+ };
85
+ var defaultRetryPolicy = {
86
+ maxAttempts: 4,
87
+ backoff: "exponential",
88
+ baseDelayMs: 250,
89
+ maxDelayMs: 4e3,
90
+ retryableErrors: [ErrorCodes.NODE_BUSY, ErrorCodes.REQUEST_TIMEOUT],
91
+ maxTotalMs: 12e3
92
+ };
93
+ function serviceError(code, message, service, options) {
94
+ return {
95
+ code,
96
+ message,
97
+ service,
98
+ cause: options?.cause,
99
+ meta: options?.meta
100
+ };
101
+ }
102
+
103
+ // src/diagnostics.ts
104
+ var REDACTED = "[REDACTED]";
105
+ var SAFE_NUMBER_FIELDS = /* @__PURE__ */ new Set([
106
+ "duration",
107
+ "durationMs",
108
+ "endedAt",
109
+ "startedAt",
110
+ "status",
111
+ "timestamp",
112
+ "latencyMs",
113
+ "stalenessMs",
114
+ "count",
115
+ "changes",
116
+ "deleted",
117
+ "fetched",
118
+ "pages",
119
+ "lagMs",
120
+ "pendingCleared",
121
+ "partitions",
122
+ "remaining",
123
+ "expiryMs"
124
+ ]);
125
+ var SAFE_BOOLEAN_FIELDS = /* @__PURE__ */ new Set(["authenticated", "ok", "persisted", "purgePending"]);
126
+ var SAFE_REPLICATION_FIELDS = {
127
+ source: ["replica", "network", "none"],
128
+ reason: ["hit", "absent", "deleted", "content_missing", "coverage_incomplete", "not_covered", "pending_write", "grant_missing", "grant_expired", "grant_revoked", "grant_not_yet_valid", "stale", "replica_error", "runtime_unsupported", "replica_unavailable", "unsupported_option", "network_cursor", "cursor_restart", "aborted", "REPLICA_BEHIND_OWN_WRITES", "REPLICA_UNPROVEN_SINCE_START", "NETWORK_REQUESTED", "replica_gated", "sign_out", "account_switch", "consent_expired", "startup"],
129
+ op: ["get", "list", "put", "delete", "batchPut"],
130
+ trigger: ["start", "interval", "stale_read", "manual", "peer"],
131
+ origin: ["peer"],
132
+ outcome: ["found", "not_found", "error", "committed", "failed", "ambiguous", "ok", "busy", "aborted"],
133
+ class: ["offline", "authority", "storage", "node", "lifecycle"],
134
+ state: ["opened", "grant_installed", "grant_missing", "runtime_unsupported", "unavailable", "revoked", "recreated", "purged", "purge_failed", "pending_store_error", "pinned", "pending_cleared", "closed", "idle", "ready", "gated"],
135
+ action: ["minted", "renewed", "renewal_refused", "renewal_deferred", "capped", "expired", "consent_deadline_reached", "denied", "deny_stale", "deny_failed", "database_deleted", "marker_set", "marker_cleared", "unguarded", "drained", "erased", "erase_failed", "activation_blocked", "activated", "recovered"],
136
+ gate: ["not_established", "stamp_stale", "principal_mismatch", "purge_pending", "logout_pending", "gate_unavailable"],
137
+ kind: ["value", "missing_local", "extra_local", "keys", "order"],
138
+ coverage: ["empty", "bootstrapping", "complete"],
139
+ authority: ["valid", "expired", "revoked", "not-yet-valid"],
140
+ code: ["NETWORK_ERROR", "TIMEOUT", "ABORTED", "REPLICA_BUSY", "REPLICA_BEHIND_OWN_WRITES", "REPLICA_UNPROVEN_SINCE_START", "NETWORK_REQUESTED", "INTEGRITY_ERROR", "STORAGE_ERROR", "REPLICA_CLOSED", "RUNTIME_UNSUPPORTED", "GRANT_EXPIRED", "GRANT_REVOKED", "GRANT_NOT_YET_VALID", "GRANT_MISSING", "GRANT_INVALID", "SOURCE_CHANGED", "PROTOCOL_ERROR", "SCOPE_VIOLATION", "NODE_ERROR", "CONTENT_MISMATCH", "STORAGE_FULL", "REPLICA_GATED", "RESET_REQUIRED", "STORAGE_BUDGET", "CONSENT_EXPIRED"],
141
+ pendingState: ["in_flight", "committed", "ambiguous"],
142
+ strategy: ["session", "minted", "installed"],
143
+ verify: ["match", "diverged", "error", "aborted"]
144
+ };
145
+ function finiteNumber(value) {
146
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
147
+ }
148
+ function isBinaryData(value) {
149
+ try {
150
+ return Array.isArray(value) || ArrayBuffer.isView(value) || value instanceof ArrayBuffer;
151
+ } catch {
152
+ return true;
153
+ }
154
+ }
155
+ function read(value, key) {
156
+ try {
157
+ return value[key];
158
+ } catch {
159
+ return REDACTED;
160
+ }
161
+ }
162
+ function projectDiagnosticError(error) {
163
+ if (typeof error !== "object" || error === null) return {};
164
+ const status = finiteNumber(read(error, "status"));
165
+ return status !== void 0 && status >= 100 && status <= 599 ? { status } : {};
166
+ }
167
+ function projectDiagnosticData(value) {
168
+ if (value === null || value === void 0) return value;
169
+ if (typeof value !== "object") return REDACTED;
170
+ try {
171
+ if (isBinaryData(value)) return REDACTED;
172
+ const projected = {};
173
+ for (const key of SAFE_NUMBER_FIELDS) {
174
+ const number = finiteNumber(read(value, key));
175
+ if (number !== void 0) projected[key] = number;
176
+ }
177
+ for (const key of SAFE_BOOLEAN_FIELDS) {
178
+ const boolean = read(value, key);
179
+ if (typeof boolean === "boolean") projected[key] = boolean;
180
+ }
181
+ for (const [key, allowed] of Object.entries(SAFE_REPLICATION_FIELDS)) {
182
+ const candidate = read(value, key);
183
+ if (typeof candidate === "string" && allowed.includes(candidate)) {
184
+ projected[key] = candidate;
185
+ }
186
+ }
187
+ const url = read(value, "url");
188
+ if (url !== void 0) projected.url = REDACTED;
189
+ const error = read(value, "error");
190
+ if (error !== void 0) projected.error = projectDiagnosticError(error);
191
+ return projected;
192
+ } catch {
193
+ return REDACTED;
194
+ }
195
+ }
196
+
197
+ // src/debug.ts
198
+ var DEBUG_FLAG = "TinyCloud_debug";
199
+ var MAX_EVENTS = 1e3;
200
+ function getGlobal() {
201
+ return globalThis;
202
+ }
203
+ function nowMs() {
204
+ const performanceNow = globalThis.performance?.now?.bind(globalThis.performance);
205
+ return typeof performanceNow === "function" ? performanceNow() : Date.now();
206
+ }
207
+ function isTrue(value) {
208
+ return value === true || value === "true" || value === "1";
209
+ }
210
+ function getProcessDebugFlag() {
211
+ const processLike = globalThis.process;
212
+ return processLike?.env?.[DEBUG_FLAG];
213
+ }
214
+ function isBrowserWindow() {
215
+ const global = getGlobal();
216
+ return global.window === globalThis;
217
+ }
218
+ function getLocalStorage() {
219
+ if (!isBrowserWindow()) {
220
+ return void 0;
221
+ }
222
+ try {
223
+ return globalThis.localStorage;
224
+ } catch {
225
+ return void 0;
226
+ }
227
+ }
228
+ function getStoredDebugFlag() {
229
+ const storage = getLocalStorage();
230
+ if (!storage) {
231
+ return void 0;
232
+ }
233
+ try {
234
+ return storage.getItem(DEBUG_FLAG);
235
+ } catch {
236
+ return void 0;
237
+ }
238
+ }
239
+ function setStoredDebugFlag(enabled) {
240
+ const storage = getLocalStorage();
241
+ if (!storage) {
242
+ return;
243
+ }
244
+ try {
245
+ if (enabled) {
246
+ storage.setItem(DEBUG_FLAG, "true");
247
+ } else {
248
+ storage.removeItem(DEBUG_FLAG);
249
+ }
250
+ } catch {
251
+ }
252
+ }
253
+ function shouldStartEnabled() {
254
+ const global = getGlobal();
255
+ return isTrue(global[DEBUG_FLAG]) || isTrue(getStoredDebugFlag()) || isTrue(getProcessDebugFlag());
256
+ }
257
+ var TinyCloudDebugLogger = class {
258
+ constructor() {
259
+ this.enabled = shouldStartEnabled();
260
+ this.sequence = 0;
261
+ this.events = [];
262
+ this.maxEvents = MAX_EVENTS;
263
+ }
264
+ isEnabled() {
265
+ return this.enabled || isTrue(getGlobal()[DEBUG_FLAG]) || isTrue(getProcessDebugFlag());
266
+ }
267
+ enable(options = {}) {
268
+ this.enabled = true;
269
+ getGlobal()[DEBUG_FLAG] = true;
270
+ if (options.persist !== false) {
271
+ setStoredDebugFlag(true);
272
+ }
273
+ this.log("debug.enabled", { persisted: options.persist !== false });
274
+ }
275
+ disable(options = {}) {
276
+ this.log("debug.disabled", { persisted: options.persist !== false });
277
+ this.enabled = false;
278
+ getGlobal()[DEBUG_FLAG] = false;
279
+ if (options.persist !== false) {
280
+ setStoredDebugFlag(false);
281
+ }
282
+ }
283
+ clear() {
284
+ this.events = [];
285
+ }
286
+ getLogs() {
287
+ return [...this.events];
288
+ }
289
+ log(event, data, message) {
290
+ if (!this.isEnabled()) {
291
+ return void 0;
292
+ }
293
+ return this.record({
294
+ event,
295
+ data,
296
+ message,
297
+ level: "debug",
298
+ timestamp: Date.now(),
299
+ timestampIso: (/* @__PURE__ */ new Date()).toISOString()
300
+ });
301
+ }
302
+ startTimer(event, data) {
303
+ if (!this.isEnabled()) {
304
+ return { stop: () => void 0 };
305
+ }
306
+ const startedAt = nowMs();
307
+ this.log(`${event}.start`, data);
308
+ return {
309
+ stop: (finishData) => {
310
+ const endedAt = nowMs();
311
+ return this.record({
312
+ event: `${event}.end`,
313
+ data: finishData,
314
+ durationMs: endedAt - startedAt,
315
+ startedAt,
316
+ endedAt,
317
+ level: "debug",
318
+ timestamp: Date.now(),
319
+ timestampIso: (/* @__PURE__ */ new Date()).toISOString()
320
+ });
321
+ }
322
+ };
323
+ }
324
+ async timeAsync(event, operation, data) {
325
+ if (!this.isEnabled()) {
326
+ return operation();
327
+ }
328
+ const timer = this.startTimer(event, data);
329
+ try {
330
+ const result = await operation();
331
+ timer.stop({ ok: true });
332
+ return result;
333
+ } catch (error) {
334
+ timer.stop({ ok: false, error });
335
+ throw error;
336
+ }
337
+ }
338
+ time(event, operation, data) {
339
+ if (!this.isEnabled()) {
340
+ return operation();
341
+ }
342
+ const timer = this.startTimer(event, data);
343
+ try {
344
+ const result = operation();
345
+ timer.stop({ ok: true });
346
+ return result;
347
+ } catch (error) {
348
+ timer.stop({ ok: false, error });
349
+ throw error;
350
+ }
351
+ }
352
+ record(event) {
353
+ const debugEvent = {
354
+ ...event,
355
+ ...event.data === void 0 ? {} : { data: projectDiagnosticData(event.data) },
356
+ ...event.message === void 0 ? {} : { message: "[REDACTED]" },
357
+ sequence: ++this.sequence
358
+ };
359
+ this.events.push(debugEvent);
360
+ if (this.events.length > this.maxEvents) {
361
+ this.events.splice(0, this.events.length - this.maxEvents);
362
+ }
363
+ try {
364
+ globalThis.console?.debug?.("[TinyCloud]", debugEvent.event, debugEvent);
365
+ } catch {
366
+ }
367
+ return debugEvent;
368
+ }
369
+ };
370
+ var tinyCloudDebugLogger = new TinyCloudDebugLogger();
371
+ function enableTinyCloudDebug(options) {
372
+ tinyCloudDebugLogger.enable(options);
373
+ return tinyCloudDebugLogger;
374
+ }
375
+ function disableTinyCloudDebug(options) {
376
+ tinyCloudDebugLogger.disable(options);
377
+ return tinyCloudDebugLogger;
378
+ }
379
+ function getTinyCloudDebugLogs() {
380
+ return tinyCloudDebugLogger.getLogs();
381
+ }
382
+ function clearTinyCloudDebugLogs() {
383
+ tinyCloudDebugLogger.clear();
384
+ }
385
+ function installTinyCloudDebugGlobals() {
386
+ const global = getGlobal();
387
+ global.TinyCloudDebug = tinyCloudDebugLogger;
388
+ global.enableTinyCloudDebug = enableTinyCloudDebug;
389
+ global.disableTinyCloudDebug = disableTinyCloudDebug;
390
+ global.getTinyCloudDebugLogs = getTinyCloudDebugLogs;
391
+ global.clearTinyCloudDebugLogs = clearTinyCloudDebugLogs;
392
+ }
393
+ installTinyCloudDebugGlobals();
394
+
395
+ // src/responseErrors.ts
396
+ function responseErrorMeta(status, statusText, errorText) {
397
+ const meta = { status, statusText };
398
+ const snippet = compactMessage(errorText);
399
+ if (snippet) {
400
+ meta.responseSnippet = snippet.slice(0, 300);
401
+ }
402
+ return meta;
403
+ }
404
+ function compactMessage(message) {
405
+ const compact = message.replace(/\s+/g, " ").trim();
406
+ return compact.length > 500 ? `${compact.slice(0, 500)}...` : compact;
407
+ }
408
+
409
+ // src/errors.ts
410
+ var import_bootstrap = require("@tinycloud/bootstrap");
411
+ function nodeRetryError(service, message, response, errorText) {
412
+ const body = errorText.trim();
413
+ const [, token, stage] = /^([a-z_]+)(?::\s*([a-z_]+):)?/.exec(body) ?? [];
414
+ let code;
415
+ if (response.status === 503 && (token === "sql_busy" || token === "unavailable_retryable" || token === "read_audit_busy")) {
416
+ code = ErrorCodes.NODE_BUSY;
417
+ } else if (response.status === 503 && token === "request_timeout") {
418
+ code = ErrorCodes.REQUEST_TIMEOUT;
419
+ } else if (response.status === 500 && token === "outcome_unknown") {
420
+ code = ErrorCodes.OUTCOME_UNKNOWN;
421
+ } else {
422
+ return void 0;
423
+ }
424
+ const retryable = code !== ErrorCodes.OUTCOME_UNKNOWN;
425
+ const retryAfter = response.headers.get("retry-after")?.trim();
426
+ return serviceError(code, message, service, {
427
+ meta: {
428
+ ...responseErrorMeta(response.status, response.statusText, errorText),
429
+ retryable,
430
+ ...token === "sql_busy" && stage ? { stage } : {},
431
+ ...retryable && retryAfter && /^\d{1,9}$/.test(retryAfter) ? { retryAfterMs: Number(retryAfter) * 1e3 } : {}
432
+ }
433
+ });
434
+ }
435
+ var grantableActions = new Set(
436
+ import_bootstrap.CAPABILITY_REGISTRY.map(({ urn }) => urn).filter((urn) => !urn.endsWith("/*"))
437
+ );
438
+ var capabilityServices = new Set(
439
+ import_bootstrap.CAPABILITY_REGISTRY.map(({ service }) => service.slice("tinycloud.".length))
440
+ );
441
+
442
+ // src/sleep.ts
443
+ var abortableSleep = (ms, signal, unref = false) => new Promise((resolve) => {
444
+ const done = () => {
445
+ clearTimeout(timer);
446
+ signal.removeEventListener("abort", done);
447
+ resolve();
448
+ };
449
+ const timer = setTimeout(done, ms);
450
+ if (unref) timer.unref?.();
451
+ signal.aborted ? done() : signal.addEventListener("abort", done);
452
+ });
453
+
454
+ // src/context.ts
455
+ var ServiceContext = class {
456
+ constructor(config) {
457
+ this._session = null;
458
+ this._services = /* @__PURE__ */ new Map();
459
+ this._eventHandlers = /* @__PURE__ */ new Map();
460
+ this._abortController = new AbortController();
461
+ this._retired = false;
462
+ this._invoke = this.wrapInvoke(config.invoke);
463
+ this._invokeAny = config.invokeAny ? this.wrapInvokeAny(config.invokeAny) : void 0;
464
+ this._fetch = this.wrapFetch(config.fetch ?? globalThis.fetch.bind(globalThis));
465
+ this._hosts = config.hosts;
466
+ this._session = config.session ?? null;
467
+ this._retryPolicy = {
468
+ ...defaultRetryPolicy,
469
+ ...config.retryPolicy
470
+ };
471
+ this._telemetryEnabled = typeof config.telemetry === "boolean" ? config.telemetry : config.telemetry?.enabled === true;
472
+ this._telemetryHandler = typeof config.telemetry === "object" ? config.telemetry.onEvent : void 0;
473
+ }
474
+ // ============================================================
475
+ // Session Management
476
+ // ============================================================
477
+ /**
478
+ * Get the current session.
479
+ */
480
+ get session() {
481
+ return this._retired ? null : this._session;
482
+ }
483
+ /**
484
+ * Check if the context has an authenticated session.
485
+ */
486
+ get isAuthenticated() {
487
+ return !this._retired && this._session !== null;
488
+ }
489
+ /**
490
+ * Update the session and notify all registered services.
491
+ *
492
+ * @param session - New session or null to clear
493
+ */
494
+ setSession(session) {
495
+ this.assertActive();
496
+ this._session = session;
497
+ this.emit("session.changed", { authenticated: session !== null });
498
+ for (const service of this._services.values()) {
499
+ service.onSessionChange(session);
500
+ }
501
+ }
502
+ // ============================================================
503
+ // Platform Dependencies
504
+ // ============================================================
505
+ /**
506
+ * Get the invoke function for WASM operations.
507
+ */
508
+ get invoke() {
509
+ this.assertActive();
510
+ return this._invoke;
511
+ }
512
+ /**
513
+ * Get the multi-resource invoke function when available.
514
+ */
515
+ get invokeAny() {
516
+ this.assertActive();
517
+ return this._invokeAny;
518
+ }
519
+ /**
520
+ * Get the fetch function for HTTP requests.
521
+ */
522
+ get fetch() {
523
+ this.assertActive();
524
+ return this._fetch;
525
+ }
526
+ /**
527
+ * Get the list of TinyCloud host URLs.
528
+ */
529
+ get hosts() {
530
+ this.assertActive();
531
+ return this._hosts;
532
+ }
533
+ // ============================================================
534
+ // Service Registry
535
+ // ============================================================
536
+ /**
537
+ * Register a service with the context.
538
+ *
539
+ * @param name - Service name (e.g., 'kv')
540
+ * @param service - Service instance
541
+ */
542
+ registerService(name, service) {
543
+ this._services.set(name, service);
544
+ }
545
+ /**
546
+ * Unregister a service from the context.
547
+ *
548
+ * @param name - Service name to remove
549
+ */
550
+ unregisterService(name) {
551
+ this._services.delete(name);
552
+ }
553
+ /**
554
+ * Get a registered service by name.
555
+ *
556
+ * @param name - Service name
557
+ * @returns The service instance or undefined if not registered
558
+ */
559
+ getService(name) {
560
+ return this._services.get(name);
561
+ }
562
+ // ============================================================
563
+ // Event System (Telemetry)
564
+ // ============================================================
565
+ /**
566
+ * Emit a telemetry event.
567
+ *
568
+ * @param event - Event name
569
+ * @param data - Event data
570
+ */
571
+ emit(event, data) {
572
+ tinyCloudDebugLogger.log(event, data);
573
+ if (this._telemetryEnabled && this._telemetryHandler) {
574
+ try {
575
+ this._telemetryHandler(event, projectDiagnosticData(data));
576
+ } catch (error) {
577
+ console.error(`Error in telemetry handler for "${event}":`, error);
578
+ }
579
+ }
580
+ const handlers = this._eventHandlers.get(event);
581
+ if (handlers) {
582
+ for (const handler of handlers) {
583
+ try {
584
+ handler(data);
585
+ } catch (error) {
586
+ console.error(`Error in event handler for "${event}":`, error);
587
+ }
588
+ }
589
+ }
590
+ }
591
+ /**
592
+ * Subscribe to telemetry events.
593
+ *
594
+ * @param event - Event name to subscribe to
595
+ * @param handler - Handler function
596
+ * @returns Unsubscribe function
597
+ */
598
+ on(event, handler) {
599
+ if (!this._eventHandlers.has(event)) {
600
+ this._eventHandlers.set(event, /* @__PURE__ */ new Set());
601
+ }
602
+ this._eventHandlers.get(event).add(handler);
603
+ return () => {
604
+ const handlers = this._eventHandlers.get(event);
605
+ if (handlers) {
606
+ handlers.delete(handler);
607
+ if (handlers.size === 0) {
608
+ this._eventHandlers.delete(event);
609
+ }
610
+ }
611
+ };
612
+ }
613
+ /**
614
+ * Remove all event handlers for an event.
615
+ *
616
+ * @param event - Event name (if omitted, clears all events)
617
+ */
618
+ clearEventHandlers(event) {
619
+ if (event) {
620
+ this._eventHandlers.delete(event);
621
+ } else {
622
+ this._eventHandlers.clear();
623
+ }
624
+ }
625
+ // ============================================================
626
+ // Lifecycle
627
+ // ============================================================
628
+ /**
629
+ * Get the abort signal for cancelling operations.
630
+ */
631
+ get abortSignal() {
632
+ return this._abortController.signal;
633
+ }
634
+ get operationAbortSignal() {
635
+ return this._operationAbortSignal;
636
+ }
637
+ setOperationAbortSignal(signal) {
638
+ this._operationAbortSignal = signal;
639
+ }
640
+ /**
641
+ * Permanently retire this graph after its owner installs a replacement.
642
+ * Unlike `abort()`, retirement never creates a fresh controller, so captured
643
+ * services cannot resume calls under a superseded session.
644
+ */
645
+ retire() {
646
+ if (this._retired) return;
647
+ this._retired = true;
648
+ this._session = null;
649
+ this._abortController.abort();
650
+ for (const service of this._services.values()) {
651
+ try {
652
+ service.onSignOut();
653
+ } catch (error) {
654
+ console.error("Error retiring service:", error);
655
+ }
656
+ }
657
+ }
658
+ /**
659
+ * Abort all pending operations and notify services.
660
+ * Creates a new AbortController for future operations.
661
+ */
662
+ abort() {
663
+ if (this._retired) return;
664
+ this._abortController.abort();
665
+ this._abortController = new AbortController();
666
+ for (const service of this._services.values()) {
667
+ try {
668
+ service.onSignOut();
669
+ } catch (error) {
670
+ console.error("Error signing out service:", error);
671
+ }
672
+ }
673
+ }
674
+ /**
675
+ * Sign out - abort operations and clear session.
676
+ */
677
+ signOut() {
678
+ if (this._retired) return;
679
+ this.abort();
680
+ this.setSession(null);
681
+ this.emit("session.expired", {});
682
+ }
683
+ // ============================================================
684
+ // Retry Policy
685
+ // ============================================================
686
+ /**
687
+ * Get the retry policy configuration.
688
+ */
689
+ get retryPolicy() {
690
+ return this._retryPolicy;
691
+ }
692
+ wrapInvoke(invoke) {
693
+ return (session, service, path, action, facts) => {
694
+ this.assertActive();
695
+ if (!tinyCloudDebugLogger.isEnabled()) {
696
+ return invoke(session, service, path, action, facts);
697
+ }
698
+ const timer = tinyCloudDebugLogger.startTimer("sdk.invoke", {
699
+ service,
700
+ path,
701
+ action,
702
+ facts
703
+ });
704
+ try {
705
+ const result = invoke(session, service, path, action, facts);
706
+ timer.stop({ ok: true, service, path, action });
707
+ return result;
708
+ } catch (error) {
709
+ timer.stop({ ok: false, service, path, action, error });
710
+ throw error;
711
+ }
712
+ };
713
+ }
714
+ assertActive() {
715
+ if (this._retired) {
716
+ throw new Error("Service graph has been retired: its session was replaced or signed out.");
717
+ }
718
+ }
719
+ wrapInvokeAny(invokeAny) {
720
+ return (session, entries, facts) => {
721
+ this.assertActive();
722
+ if (!tinyCloudDebugLogger.isEnabled()) {
723
+ return invokeAny(session, entries, facts);
724
+ }
725
+ const timer = tinyCloudDebugLogger.startTimer("sdk.invokeAny", {
726
+ entries,
727
+ facts
728
+ });
729
+ try {
730
+ const result = invokeAny(session, entries, facts);
731
+ timer.stop({ ok: true, entries });
732
+ return result;
733
+ } catch (error) {
734
+ timer.stop({ ok: false, entries, error });
735
+ throw error;
736
+ }
737
+ };
738
+ }
739
+ /** Retry `NODE_BUSY` / `REQUEST_TIMEOUT`; `build` must re-sign per attempt (the node 409s a repeated invocation). */
740
+ async invokeWithRetry(url, build) {
741
+ this.assertActive();
742
+ const policy = this._retryPolicy;
743
+ const startedAt = Date.now();
744
+ let backoffMs = 0;
745
+ let signal;
746
+ for (let attempt = 1; ; attempt++) {
747
+ const init = await build();
748
+ signal ?? (signal = this.combineSignals(init.signal));
749
+ const response = await this._fetch(url, init);
750
+ const done = () => {
751
+ try {
752
+ response.retry = { attempts: attempt, backoffMs };
753
+ } catch {
754
+ }
755
+ return response;
756
+ };
757
+ if (attempt >= policy.maxAttempts || response.status !== 503 || !response.clone) return done();
758
+ const text = await response.clone().text().catch(() => "");
759
+ const error = nodeRetryError("", "", response, text);
760
+ if (!error || error.meta?.retryable !== true || !policy.retryableErrors.includes(error.code)) {
761
+ return done();
762
+ }
763
+ const remaining = (policy.maxTotalMs ?? Infinity) - (Date.now() - startedAt);
764
+ if (remaining <= 0) return done();
765
+ const retryAfterMs = error.meta.retryAfterMs;
766
+ const step = Math.min(
767
+ policy.maxDelayMs,
768
+ policy.backoff === "exponential" ? policy.baseDelayMs * 2 ** (attempt - 1) : policy.backoff === "linear" ? policy.baseDelayMs * attempt : 0
769
+ );
770
+ const delay = Math.min(
771
+ typeof retryAfterMs === "number" ? retryAfterMs : Math.random() * step,
772
+ remaining
773
+ );
774
+ response.body?.cancel?.()?.catch?.(() => {
775
+ });
776
+ await abortableSleep(delay, signal);
777
+ if (signal.aborted) throw signal.reason;
778
+ backoffMs += delay;
779
+ }
780
+ }
781
+ wrapFetch(fetchFn) {
782
+ return async (url, init) => {
783
+ this.assertActive();
784
+ const request = {
785
+ ...init,
786
+ signal: this.combineSignals(init?.signal)
787
+ };
788
+ if (!tinyCloudDebugLogger.isEnabled()) {
789
+ return fetchFn(url, request);
790
+ }
791
+ const meta = { url, method: request.method ?? "GET" };
792
+ const timer = tinyCloudDebugLogger.startTimer("sdk.fetch", { ...meta, init: request });
793
+ try {
794
+ const response = await fetchFn(url, request);
795
+ timer.stop({
796
+ ...meta,
797
+ ok: response.ok,
798
+ status: response.status,
799
+ statusText: response.statusText
800
+ });
801
+ return response;
802
+ } catch (error) {
803
+ timer.stop({ ...meta, ok: false, error });
804
+ throw error;
805
+ }
806
+ };
807
+ }
808
+ combineSignals(signal) {
809
+ if (!signal) return this._abortController.signal;
810
+ const combined = new AbortController();
811
+ const abort = (source) => combined.abort(source.reason);
812
+ if (signal.aborted) {
813
+ abort(signal);
814
+ } else if (this._abortController.signal.aborted) {
815
+ abort(this._abortController.signal);
816
+ } else {
817
+ signal.addEventListener("abort", () => abort(signal), { once: true });
818
+ this._abortController.signal.addEventListener(
819
+ "abort",
820
+ () => abort(this._abortController.signal),
821
+ { once: true }
822
+ );
823
+ }
824
+ return combined.signal;
825
+ }
826
+ };
827
+ // Annotate the CommonJS export names for ESM import in node:
828
+ 0 && (module.exports = {
829
+ ServiceContext
830
+ });
831
+ //# sourceMappingURL=context.cjs.map