@tinycloud/sdk-core 2.11.0-beta.3 → 2.11.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.
package/dist/index.cjs CHANGED
@@ -12266,6 +12266,60 @@ var AccountService = class {
12266
12266
  await this.upsertSpaceIndexQuietly(registered);
12267
12267
  return (0, import_sdk_services6.ok)(registered);
12268
12268
  },
12269
+ /**
12270
+ * Register multiple spaces in one KV batch write + one multi-row index
12271
+ * write, instead of one `register()` round trip per space (TC-373).
12272
+ *
12273
+ * The stored records are computed exactly once and reused for both the
12274
+ * batch attempt and any reconciliation below, so a fallback never mints a
12275
+ * fresh timestamp for a space that already has a written record — a
12276
+ * second `spaceRecordFromInput` call would produce a new content hash and
12277
+ * leave the first attempt's blocks orphaned.
12278
+ */
12279
+ registerBatch: async (spaces) => {
12280
+ if (spaces.length === 0) return (0, import_sdk_services6.ok)({ spaces: [] });
12281
+ await this.config.ensureAccountSpaceHosted?.();
12282
+ const kvResult = this.accountKV();
12283
+ if (!kvResult.ok) return kvResult;
12284
+ const stored = spaces.map((space) => spaceRecordFromInput(space));
12285
+ const registered = stored.map(
12286
+ (record) => spaceFromRecord(spaceKey(record.space_id), record)
12287
+ );
12288
+ const items = stored.map((record) => ({
12289
+ key: spaceKey(record.space_id),
12290
+ value: record
12291
+ }));
12292
+ const batchResult = await kvResult.data.batchPut(items);
12293
+ if (batchResult.ok) {
12294
+ await this.upsertSpacesIndexQuietly(registered);
12295
+ return (0, import_sdk_services6.ok)({ spaces: registered });
12296
+ }
12297
+ if (!isAmbiguousBatchFailure(batchResult.error)) {
12298
+ return accountErr(batchResult.error);
12299
+ }
12300
+ const reconcileFailures = [];
12301
+ for (const record of stored) {
12302
+ const written = await kvResult.data.put(spaceKey(record.space_id), record);
12303
+ if (!written.ok) {
12304
+ reconcileFailures.push(`${record.space_id}: ${written.error.message}`);
12305
+ }
12306
+ }
12307
+ if (reconcileFailures.length > 0) {
12308
+ return (0, import_sdk_services6.err)(
12309
+ (0, import_sdk_services6.serviceError)(
12310
+ "KV_BATCH_RECONCILE_FAILED",
12311
+ `KV batch write for ${items.length} space(s) failed ambiguously (${batchResult.error.message}) and per-space reconciliation also failed for: ${reconcileFailures.join("; ")}`,
12312
+ SERVICE_NAME2,
12313
+ { cause: batchResult.error.cause, meta: { originalError: batchResult.error } }
12314
+ )
12315
+ );
12316
+ }
12317
+ console.warn(
12318
+ `[AccountService] KV batch write for ${items.length} space(s) returned an ambiguous error and was reconciled via per-space fallback. Original error: ${batchResult.error.message}`
12319
+ );
12320
+ await this.upsertSpacesIndexQuietly(registered);
12321
+ return (0, import_sdk_services6.ok)({ spaces: registered, recoveredFromBatchError: batchResult.error });
12322
+ },
12269
12323
  syncAccessible: async () => {
12270
12324
  const listed = await this.config.getSpaces().list();
12271
12325
  if (!listed.ok) return accountErr(listed.error);
@@ -12652,6 +12706,47 @@ var AccountService = class {
12652
12706
  if (!written.ok) return accountErr(written.error);
12653
12707
  return (0, import_sdk_services6.ok)(void 0);
12654
12708
  }
12709
+ async upsertSpacesIndexQuietly(spaces) {
12710
+ await ignoreIndexFailure(() => this.upsertSpacesIndex(spaces));
12711
+ }
12712
+ /**
12713
+ * Upsert multiple space index rows in ONE multi-row `INSERT OR REPLACE`
12714
+ * statement (TC-373), rather than one statement per space. This matters
12715
+ * because `IDatabaseHandle.batch()` executes its statement list as a plain
12716
+ * loop, not a single database transaction — an N-statement batch is not
12717
+ * atomic. A single multi-row INSERT is one SQLite statement, so it applies
12718
+ * atomically: all rows or none.
12719
+ */
12720
+ async upsertSpacesIndex(spaces) {
12721
+ if (spaces.length === 0) return (0, import_sdk_services6.ok)(void 0);
12722
+ const dbResult = this.accountDb();
12723
+ if (!dbResult.ok) return (0, import_sdk_services6.ok)(void 0);
12724
+ const schema = await this.ensureAccountIndex(dbResult.data);
12725
+ if (!schema.ok) return schema;
12726
+ const placeholders = spaces.map(() => "(?, ?, ?, ?, ?, ?, ?, ?, ?)").join(", ");
12727
+ const params = spaces.flatMap((space) => {
12728
+ const updatedAt = space.updatedAt ?? (/* @__PURE__ */ new Date()).toISOString();
12729
+ return [
12730
+ space.spaceId,
12731
+ space.name,
12732
+ space.ownerDid,
12733
+ space.type,
12734
+ JSON.stringify(space.permissions),
12735
+ space.status,
12736
+ space.registeredAt ?? updatedAt,
12737
+ updatedAt,
12738
+ space.expiresAt?.toISOString() ?? null
12739
+ ];
12740
+ });
12741
+ const written = await dbResult.data.batch([
12742
+ {
12743
+ sql: `INSERT OR REPLACE INTO spaces (space_id, name, owner_did, type, permissions_json, status, registered_at, updated_at, expires_at) VALUES ${placeholders}`,
12744
+ params
12745
+ }
12746
+ ]);
12747
+ if (!written.ok) return accountErr(written.error);
12748
+ return (0, import_sdk_services6.ok)(void 0);
12749
+ }
12655
12750
  async deleteSpaceIndexQuietly(spaceId) {
12656
12751
  await ignoreIndexFailure(() => this.deleteSpaceIndex(spaceId));
12657
12752
  }
@@ -12978,6 +13073,17 @@ function accountErr(error) {
12978
13073
  function isMissingIndexError(error) {
12979
13074
  return /no such table:/i.test(error.message);
12980
13075
  }
13076
+ var AMBIGUOUS_WRITE_STATUSES = /* @__PURE__ */ new Set([500, 502, 503, 504, 520, 522, 524]);
13077
+ function isAmbiguousBatchFailure(error) {
13078
+ if (error.code === import_sdk_services6.ErrorCodes.NETWORK_ERROR || error.code === import_sdk_services6.ErrorCodes.TIMEOUT) {
13079
+ return error.meta?.requestMayHaveDispatched === true;
13080
+ }
13081
+ if (error.code === import_sdk_services6.ErrorCodes.KV_WRITE_FAILED) {
13082
+ const status = error.meta?.status;
13083
+ return typeof status === "number" && AMBIGUOUS_WRITE_STATUSES.has(status);
13084
+ }
13085
+ return false;
13086
+ }
12981
13087
  async function ignoreIndexFailure(task) {
12982
13088
  try {
12983
13089
  await task();
@@ -15350,13 +15456,14 @@ async function discoverLocalTinyCloudNode(options2 = {}) {
15350
15456
  }
15351
15457
  const fetchFn = options2.fetch ?? globalThis.fetch;
15352
15458
  const identityStore = options2.identityStore ?? defaultLocalNodeIdentityStore;
15353
- const staticCandidates = [
15354
- {
15459
+ const staticCandidates = [];
15460
+ if (options2.localNodeUrl) {
15461
+ staticCandidates.push({
15355
15462
  source: "local-loopback",
15356
- url: options2.localNodeUrl ?? DEFAULT_LOCAL_NODE_URL,
15463
+ url: options2.localNodeUrl,
15357
15464
  timeoutMs: LOCAL_LOOPBACK_PROBE_TIMEOUT_MS
15358
- }
15359
- ];
15465
+ });
15466
+ }
15360
15467
  if (options2.localLinkName) {
15361
15468
  if (isValidDnsLabel(options2.localLinkName)) {
15362
15469
  staticCandidates.push({