@tinycloud/sdk-services 3.2.0-beta.3 → 3.2.0-beta.5

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.
@@ -77,16 +77,17 @@ ${canonical.space}
77
77
  ${canonical.principal}`;
78
78
  }
79
79
 
80
- // src/kv/replication/scope.ts
80
+ // src/kv/replication/prefix.ts
81
81
  function kvPrefixCovers(prefix, key) {
82
- if (prefix === "") return true;
83
- if (prefix.endsWith("/")) return key.startsWith(prefix);
84
- return key === prefix || key.startsWith(`${prefix}/`);
82
+ return prefix === "/" || key.startsWith(prefix);
85
83
  }
84
+
85
+ // src/kv/replication/scope.ts
86
86
  function listRangeCovers(path, key) {
87
- return key === path || key.startsWith(path.endsWith("/") ? path : `${path}/`);
87
+ return path === "" || key === path || key.startsWith(path.endsWith("/") ? path : `${path}/`);
88
88
  }
89
89
  function requiresSecretsOptIn(space, prefix) {
90
+ if (prefix === "/") return true;
90
91
  if (space === "secrets" || space.endsWith(":secrets")) return true;
91
92
  return prefix.split("/", 1)[0] === "vault";
92
93
  }
@@ -371,9 +372,9 @@ function cursorRestart() {
371
372
  }
372
373
  function localList(handle, space, path, limit, cursor, validateMeta) {
373
374
  const after = cursor ? decodeTcr1(cursor)?.last : void 0;
374
- const exact = !path.endsWith("/") && (after === void 0 || utf8Compare(path, after) > 0) ? awaitExact(handle, path, after, validateMeta) : Promise.resolve(false);
375
+ const exact = path !== "" && !path.endsWith("/") && (after === void 0 || utf8Compare(path, after) > 0) ? awaitExact(handle, path, after, validateMeta) : Promise.resolve(false);
375
376
  return exact.then(async (hasExact) => {
376
- const child = path.endsWith("/") ? path : `${path}/`;
377
+ const child = path === "" ? "" : path.endsWith("/") ? path : `${path}/`;
377
378
  const want = limit === void 0 ? void 0 : limit + 1 - (hasExact ? 1 : 0);
378
379
  const children = await handle.list({ prefix: child, after, limit: want });
379
380
  validateMeta(children.meta);
@@ -390,9 +391,7 @@ async function awaitExact(handle, path, after, validateMeta) {
390
391
  return result.keys[0] === path;
391
392
  }
392
393
  function listPathCovered(prefixes, path) {
393
- const descendant = path.endsWith("/") ? path : `${path}/`;
394
- const covering = prefixes.filter((prefix) => kvPrefixCovers(prefix, path) && kvPrefixCovers(prefix, descendant));
395
- return covering.length === 1 ? covering[0] : void 0;
394
+ return prefixes.find((prefix) => kvPrefixCovers(prefix, path));
396
395
  }
397
396
 
398
397
  // src/kv/replication/events.ts
@@ -421,6 +420,7 @@ function raceSignal(job, signal) {
421
420
  }
422
421
  function createKVReplication(deps) {
423
422
  const { options, mode, storage, identity, session, authority, pending, scheduler } = deps;
423
+ const emit = (event) => emitEvent(deps.emit, scheduler, event);
424
424
  const replicas = /* @__PURE__ */ new Map();
425
425
  const statusInspections = /* @__PURE__ */ new Set();
426
426
  let isClosed = false;
@@ -429,7 +429,6 @@ function createKVReplication(deps) {
429
429
  let previous = deps.previous;
430
430
  let pinnedStateReported = false;
431
431
  const inProcessProof = /* @__PURE__ */ new Set();
432
- const emit = (event) => emitEvent(deps.emit, scheduler, event);
433
432
  const configuredPrefix = (path) => options.prefixes.find((prefix) => kvPrefixCovers(prefix, path));
434
433
  function recordRevocation(prefix, error) {
435
434
  if (errorCode(error) !== "GRANT_REVOKED") return;
@@ -439,6 +438,20 @@ function createKVReplication(deps) {
439
438
  state.lastError = "GRANT_REVOKED";
440
439
  replicas.set(prefix, state);
441
440
  }
441
+ function markRootUnsupported(prefix) {
442
+ const state = replicas.get(prefix) ?? {};
443
+ const firstRefusal = state.lastError !== "REPLICATION_ROOT_UNSUPPORTED";
444
+ state.reason = "replica_unavailable";
445
+ state.lastError = "REPLICATION_ROOT_UNSUPPORTED";
446
+ replicas.set(prefix, state);
447
+ if (firstRefusal) {
448
+ emit({ type: "replication.state", space: session.space, replica: prefix, state: "unavailable", code: "REPLICATION_ROOT_UNSUPPORTED" });
449
+ }
450
+ }
451
+ function unsupportedRoot(prefix) {
452
+ markRootUnsupported(prefix);
453
+ return Object.assign(new Error("This node doesn't support whole-space sync yet."), { code: "REPLICATION_ROOT_UNSUPPORTED" });
454
+ }
442
455
  async function open(prefix) {
443
456
  if (isClosed || purgeStarted) throw Object.assign(new Error("Replication controller is closed"), { code: "REPLICA_CLOSED" });
444
457
  let state = replicas.get(prefix);
@@ -472,6 +485,9 @@ function createKVReplication(deps) {
472
485
  state.strategy = "session";
473
486
  }
474
487
  const spec = { identity, space: session.space, prefix, allowSecrets: options.allowSecrets, ...device ? { device } : {}, ...deps.fetch ? { fetch: deps.fetch } : {} };
488
+ if (prefix === "/" && deps.nodeFeatures !== void 0 && !deps.nodeFeatures.includes("kv-sync-root")) {
489
+ throw unsupportedRoot(prefix);
490
+ }
475
491
  const handle = await storage.open(spec);
476
492
  state.handle = handle;
477
493
  emit({ type: "replication.state", space: session.space, replica: prefix, state: "opened" });
@@ -521,7 +537,7 @@ function createKVReplication(deps) {
521
537
  state.lastError = code;
522
538
  state.failures = (state.failures ?? 0) + 1;
523
539
  if (code === "RUNTIME_UNSUPPORTED") state.unsupported = true;
524
- else if (code !== "REPLICA_CLOSED") state.retryAt = scheduler.now() + Math.min(3e5, 5e3 * 2 ** Math.min(state.failures - 1, 6));
540
+ else if (code !== "REPLICA_CLOSED" && code !== "REPLICATION_ROOT_UNSUPPORTED") state.retryAt = scheduler.now() + Math.min(3e5, 5e3 * 2 ** Math.min(state.failures - 1, 6));
525
541
  state.handle?.close().catch(() => void 0);
526
542
  state.handle = void 0;
527
543
  throw error;
@@ -538,6 +554,9 @@ function createKVReplication(deps) {
538
554
  }, options.syncIntervalMs);
539
555
  }
540
556
  async function syncPrefix(prefix, trigger, signal, context) {
557
+ if (prefix === "/" && deps.nodeFeatures !== void 0 && !deps.nodeFeatures.includes("kv-sync-root")) {
558
+ throw unsupportedRoot(prefix);
559
+ }
541
560
  const handle = await open(prefix);
542
561
  const state = replicas.get(prefix);
543
562
  if (state.sync) return state.sync;
@@ -839,7 +858,7 @@ function createKVReplication(deps) {
839
858
  observeNetworkRequested({ op: "get", space: r.space, path: r.path, reason: "NETWORK_REQUESTED", outcome, latencyMs: scheduler.now() - started });
840
859
  }
841
860
  }
842
- if (!prefix) {
861
+ if (prefix === void 0) {
843
862
  const value = await r.network();
844
863
  readEvent("get", r.path, null, "network", "not_covered", value.ok ? "found" : value.error.code === ErrorCodes.KV_NOT_FOUND ? "not_found" : "error", started);
845
864
  return value;
@@ -990,10 +1009,10 @@ function createKVReplication(deps) {
990
1009
  return cursorRestart();
991
1010
  };
992
1011
  if (cursor && decoded && (decoded.space !== r.space || decoded.path !== r.listPath)) return restart();
993
- if (!prefix || !r.listPath || r.options?.raw) {
1012
+ if (prefix === void 0 || r.options?.raw) {
994
1013
  if (cursor && decoded) return restart();
995
1014
  const result = await r.network();
996
- readEvent("list", r.listPath, prefix ?? null, "network", prefix ? "unsupported_option" : "not_covered", result.ok ? "found" : "error", started);
1015
+ readEvent("list", r.listPath, prefix ?? null, "network", prefix !== void 0 ? "unsupported_option" : "not_covered", result.ok ? "found" : "error", started);
997
1016
  return result;
998
1017
  }
999
1018
  let handle;
@@ -1088,7 +1107,7 @@ function createKVReplication(deps) {
1088
1107
  async function write(r) {
1089
1108
  const covered = r.entries.flatMap((entry) => {
1090
1109
  const prefix = configuredPrefix(entry.path);
1091
- return prefix ? [{ key: entry.path, op: r.op === "delete" ? "delete" : "put" }] : [];
1110
+ return prefix !== void 0 ? [{ key: entry.path, op: r.op === "delete" ? "delete" : "put" }] : [];
1092
1111
  });
1093
1112
  if (covered.length === 0) return r.network();
1094
1113
  await reportExistingPins();
@@ -1164,6 +1183,9 @@ function createKVReplication(deps) {
1164
1183
  const persistedSnapshot = pending.readExisting ? await pending.readExisting() : await pending.read();
1165
1184
  const snapshot = persistedSnapshot ?? emptyPendingState(identity);
1166
1185
  const entries = await Promise.all(options.prefixes.map(async (prefix) => {
1186
+ if (prefix === "/" && deps.nodeFeatures !== void 0 && !deps.nodeFeatures.includes("kv-sync-root")) {
1187
+ markRootUnsupported(prefix);
1188
+ }
1167
1189
  const records = snapshot.records.filter((r) => kvPrefixCovers(prefix, r.key));
1168
1190
  const opened = replicas.get(prefix);
1169
1191
  let local;
@@ -1178,12 +1200,13 @@ function createKVReplication(deps) {
1178
1200
  local ?? (local = inspected.local);
1179
1201
  if (inspected.missing && !opened?.reason && !opened?.authorityRevoked && !opened?.opening && !isClosed) return void 0;
1180
1202
  const revoked = opened?.authorityRevoked === true || local?.authority.state === "revoked";
1181
- const state = isClosed ? "closed" : revoked ? "revoked" : opened?.reason === "grant_missing" ? "grant_missing" : local?.coverage === "complete" && local.authority.state === "valid" ? "ready" : opened?.reason || inspected.errorCode ? "unavailable" : "idle";
1203
+ const rootUnsupported = opened?.lastError === "REPLICATION_ROOT_UNSUPPORTED";
1204
+ const state = isClosed ? "closed" : revoked ? "revoked" : rootUnsupported ? "unavailable" : opened?.reason === "grant_missing" ? "grant_missing" : local?.coverage === "complete" && local.authority.state === "valid" ? "ready" : opened?.reason || inspected.errorCode ? "unavailable" : "idle";
1182
1205
  return {
1183
1206
  prefix,
1184
1207
  state,
1185
1208
  ...opened?.reason ? { reason: opened.reason } : inspected.errorCode ? { reason: "replica_unavailable" } : {},
1186
- ...inspected.errorCode ? { errorCode: inspected.errorCode } : {},
1209
+ ...inspected.errorCode ?? opened?.lastError ? { errorCode: inspected.errorCode ?? opened?.lastError } : {},
1187
1210
  ...local ?? {},
1188
1211
  ...revoked ? { authority: { state: "revoked", expiresAt: local?.authority.expiresAt ?? null } } : {},
1189
1212
  pending: {
@@ -1198,7 +1221,7 @@ function createKVReplication(deps) {
1198
1221
  return entries.filter((entry) => entry !== void 0);
1199
1222
  }
1200
1223
  async function sync(o) {
1201
- const prefixes = o?.prefix ? options.prefixes.filter((p) => p === o.prefix) : options.prefixes;
1224
+ const prefixes = o?.prefix !== void 0 ? options.prefixes.filter((p) => p === o.prefix) : options.prefixes;
1202
1225
  await Promise.all(prefixes.map((prefix) => syncPrefix(prefix, "manual", o?.signal).then(() => void 0)));
1203
1226
  }
1204
1227
  async function clearPending2() {