@tinycloud/sdk-services 3.5.0 → 3.6.0-beta.3

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