@tinycloud/sdk-services 3.5.0-beta.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.
- package/dist/{BaseService-YLoDIq_I.d.cts → BaseService-D-29xShG.d.cts} +1 -1
- package/dist/{BaseService-BHBfRXT_.d.ts → BaseService-DO7vZ67k.d.ts} +1 -1
- package/dist/context.cjs +831 -0
- package/dist/context.cjs.map +1 -0
- package/dist/context.d.cts +168 -0
- package/dist/context.d.ts +168 -0
- package/dist/context.js +804 -0
- package/dist/context.js.map +1 -0
- package/dist/encryption/index.d.cts +2 -2
- package/dist/encryption/index.d.ts +2 -2
- package/dist/index.d.cts +7 -171
- package/dist/index.d.ts +7 -171
- package/dist/internal/decrypt-transport-response-error.d.cts +1 -1
- package/dist/internal/decrypt-transport-response-error.d.ts +1 -1
- package/dist/kv/index.d.cts +4 -4
- package/dist/kv/index.d.ts +4 -4
- package/dist/kv/replication/index.d.cts +3 -3
- package/dist/kv/replication/index.d.ts +3 -3
- package/dist/{scope-CSYhzlJw.d.cts → scope-44m8GKdi.d.cts} +1 -1
- package/dist/{scope-BRX9hLVh.d.ts → scope-CAJbG-yz.d.ts} +1 -1
- package/dist/sql/index.d.cts +2 -2
- package/dist/sql/index.d.ts +2 -2
- package/dist/telemetry/requestLog.d.cts +1 -1
- package/dist/telemetry/requestLog.d.ts +1 -1
- package/dist/{types-CJylea1R.d.cts → types-DAOyIbXu.d.cts} +1 -1
- package/dist/{types-CJylea1R.d.ts → types-DAOyIbXu.d.ts} +1 -1
- package/package.json +6 -1
package/dist/context.js
ADDED
|
@@ -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
|