@agent-native/core 0.211.0 → 0.212.0-nightly-20261021010049

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 (91) hide show
  1. package/README.md +1 -1
  2. package/dist/app-config/analytics.d.ts +1 -0
  3. package/dist/app-config/analytics.js +7 -0
  4. package/dist/app-config/schema.d.ts +1 -0
  5. package/dist/chat-threads/store.d.ts +28 -2
  6. package/dist/chat-threads/store.js +75 -13
  7. package/dist/cli/template-baseline.js +18 -5
  8. package/dist/cli/template-sync.js +100 -19
  9. package/dist/client/agent-chat/index.d.ts +2 -1
  10. package/dist/client/agent-chat/index.js +2 -1
  11. package/dist/client/chat/agentkit-protocol.js +21 -1
  12. package/dist/client/chat/run-outcome.d.ts +1 -0
  13. package/dist/client/chat/runtime.js +6 -0
  14. package/dist/client/sharing/index.d.ts +2 -1
  15. package/dist/client/sharing/index.js +2 -1
  16. package/dist/client/sharing/share-controller-helpers.d.ts +14 -1
  17. package/dist/client/sharing/share-controller-helpers.js +46 -1
  18. package/dist/client/sharing/useShareButtonController.d.ts +12 -0
  19. package/dist/client/sharing/useShareButtonController.js +76 -57
  20. package/dist/client/sharing/useShareDialogController.d.ts +3 -0
  21. package/dist/client/sharing/useShareDialogController.js +77 -49
  22. package/dist/client/sharing/useShareVisibilityTransition.d.ts +68 -0
  23. package/dist/client/sharing/useShareVisibilityTransition.js +224 -0
  24. package/dist/client/use-chat-thread-list.d.ts +40 -0
  25. package/dist/client/use-chat-thread-list.js +210 -0
  26. package/dist/client/use-chat-threads.d.ts +67 -0
  27. package/dist/client/use-chat-threads.js +141 -23
  28. package/dist/client/use-db-sync.d.ts +1 -0
  29. package/dist/client/use-db-sync.js +13 -1
  30. package/dist/collab/__fixtures__/live-copy-tasks.d.ts +1 -0
  31. package/dist/collab/__fixtures__/live-copy-tasks.js +92 -0
  32. package/dist/collab/awareness.d.ts +2 -2
  33. package/dist/collab/bounded-task-bundle.d.ts +17 -0
  34. package/dist/collab/bounded-task-bundle.js +39 -0
  35. package/dist/collab/bounded-task-source.d.ts +14 -0
  36. package/dist/collab/bounded-task-source.js +8 -0
  37. package/dist/collab/bounded-tasks.d.ts +67 -0
  38. package/dist/collab/bounded-tasks.js +439 -0
  39. package/dist/collab/client.js +2 -1
  40. package/dist/collab/index.d.ts +5 -2
  41. package/dist/collab/index.js +5 -2
  42. package/dist/collab/live-copy-tasks.d.ts +80 -0
  43. package/dist/collab/live-copy-tasks.js +330 -0
  44. package/dist/collab/live-copy.d.ts +188 -0
  45. package/dist/collab/live-copy.js +415 -0
  46. package/dist/collab/routes.d.ts +5 -0
  47. package/dist/collab/routes.js +24 -8
  48. package/dist/collab/storage.d.ts +53 -1
  49. package/dist/collab/storage.js +93 -5
  50. package/dist/collab/struct-routes.d.ts +1 -1
  51. package/dist/collab/ydoc-manager.d.ts +92 -3
  52. package/dist/collab/ydoc-manager.js +447 -38
  53. package/dist/db/client.d.ts +6 -0
  54. package/dist/db/client.js +25 -5
  55. package/dist/deploy/build.js +2 -0
  56. package/dist/extensions/actions.js +1 -0
  57. package/dist/jobs/actions/manage-recurring-job.d.ts +1 -1
  58. package/dist/mcp/agent-connected.js +5 -1
  59. package/dist/mcp/build-server.js +39 -10
  60. package/dist/notifications/routes.d.ts +1 -1
  61. package/dist/observability/routes.d.ts +1 -1
  62. package/dist/org/index.d.ts +2 -2
  63. package/dist/progress/routes.d.ts +1 -1
  64. package/dist/provider-api/actions/custom-provider-registration.d.ts +16 -16
  65. package/dist/provider-api/actions/provider-api.d.ts +10 -10
  66. package/dist/server/agent-chat-plugin.js +80 -17
  67. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  68. package/dist/server/agent-engine-default-model-route.d.ts +1 -1
  69. package/dist/server/analytics.d.ts +1 -0
  70. package/dist/server/analytics.js +18 -1
  71. package/dist/server/collab-plugin.d.ts +10 -1
  72. package/dist/server/collab-plugin.js +89 -20
  73. package/dist/server/index.d.ts +1 -1
  74. package/dist/server/realtime-token.d.ts +1 -1
  75. package/dist/shared/optional-node-builtins.d.ts +1 -0
  76. package/dist/shared/optional-node-builtins.js +3 -0
  77. package/dist/sharing/access.d.ts +1 -0
  78. package/dist/sharing/access.js +113 -95
  79. package/dist/tracking/providers.d.ts +1 -0
  80. package/dist/tracking/providers.js +2 -1
  81. package/dist/triggers/actions/manage-automation.d.ts +2 -2
  82. package/dist/vite/bounded-task-plugin.d.ts +22 -0
  83. package/dist/vite/bounded-task-plugin.js +44 -0
  84. package/dist/vite/client.js +2 -0
  85. package/dist/vitest-config.js +3 -0
  86. package/dist/workspace-connections/actions/list-workspace-user-groups.d.ts +1 -1
  87. package/docs/content/deployment-environment-variables.mdx +1 -0
  88. package/docs/content/environment-variables.mdx +1 -0
  89. package/docs/content/sharing.mdx +36 -0
  90. package/docs/content/toolkit-sharing.mdx +98 -12
  91. package/package.json +5 -3
@@ -1,7 +1,9 @@
1
1
  import * as Y from "yjs";
2
+ import { getDbExec } from "../db/client.js";
2
3
  import { emitCollabUpdate } from "./emitter.js";
3
4
  import { applyJsonDiff, applyJsonPatch, yDocToJson, initYDocWithJson, } from "./json-to-yjs.js";
4
- import { loadYDocRecord, loadYDocRecordWithClient, loadYDocVersion, trySaveYDocState, trySaveYDocStateWithClient, } from "./storage.js";
5
+ import { applyLiveCopyUpdate, assertLiveCopy, assertOwnTransaction, commitLiveCopy, editLiveCopyState, liveCopiesDefined, liveCopyBindingFor, LiveCopyError, persistLiveCopyCall, readLiveCopyState, readStoredLiveCopy, liveCopyWriteRefused, refuseLiveCopyWrite, sameStamp, seedLiveCopyState, } from "./live-copy.js";
6
+ import { collabStateDigest, ensureTable, loadYDocRecord, loadYDocRecordWithClient, loadYDocStamp, loadYDocVersion, saveYDocState as saveStoredYDocState, trySaveYDocState, trySaveYDocStateWithClient, } from "./storage.js";
5
7
  import { uint8ArrayToBase64 } from "./storage.js";
6
8
  import { applyTextToYDoc, initYDocWithText } from "./text-to-yjs.js";
7
9
  import { searchAndReplaceInYXml, extractTextFromYXml } from "./xml-ops.js";
@@ -46,6 +48,8 @@ export function computeTextEditDescriptor(oldText, newText) {
46
48
  const COMPACTION_RATIO = 4;
47
49
  const _cache = new Map();
48
50
  const _writeLocks = new Map();
51
+ /** Writes holding or waiting for each document's lock. */
52
+ const _writesPending = new Map();
49
53
  const _refreshLocks = new Map();
50
54
  const _loadLocks = new Map();
51
55
  function evictIfNeeded() {
@@ -65,7 +69,16 @@ function evictIfNeeded() {
65
69
  _cache.delete(oldest);
66
70
  }
67
71
  }
68
- async function withDocWriteLock(docId, fn) {
72
+ /**
73
+ * Runs `fn` alone among this instance's writes to `docId`. A live copy's
74
+ * writes queue here, ahead of its runner, so the runner's own queue never
75
+ * sees them; the binding bounds them instead.
76
+ */
77
+ async function withDocWriteLock(docId, fn, refusePath) {
78
+ // A write takes its place in line before its first await, so writes keep
79
+ // their call order and a slow `appliesTo` cannot let a burst past the bound.
80
+ const ahead = _writesPending.get(docId) ?? 0;
81
+ _writesPending.set(docId, ahead + 1);
69
82
  const previous = _writeLocks.get(docId) ?? Promise.resolve();
70
83
  let release;
71
84
  const current = new Promise((resolve) => {
@@ -73,15 +86,43 @@ async function withDocWriteLock(docId, fn) {
73
86
  });
74
87
  const chained = previous.catch(() => { }).then(() => current);
75
88
  _writeLocks.set(docId, chained);
76
- await previous.catch(() => { });
89
+ // Not in `finally`: a refused write releases while earlier writes still run.
90
+ void chained.then(() => {
91
+ if (_writeLocks.get(docId) === chained)
92
+ _writeLocks.delete(docId);
93
+ });
77
94
  try {
95
+ if (liveCopiesDefined()) {
96
+ const binding = await liveCopyBindingFor(docId);
97
+ // Refused outright, not as a full queue a retry could get past.
98
+ if (binding && refusePath !== undefined) {
99
+ throw liveCopyWriteRefused(docId, refusePath);
100
+ }
101
+ // Before waiting: from a binding's persist, which runs in its commit's
102
+ // transaction, this write would wait on the lock that commit holds.
103
+ if (binding)
104
+ assertOwnTransaction(docId, "This write");
105
+ if (binding && ahead > binding.maxWaitingWrites) {
106
+ throw new LiveCopyError("build_failed", docId, `Live copy ${docId} already has ${binding.maxWaitingWrites} writes waiting; try again shortly.`, { reason: "queue_full" });
107
+ }
108
+ }
109
+ await previous.catch(() => { });
110
+ // Checked under the lock so a binding defined while this write waited
111
+ // still refuses it.
112
+ if (refusePath !== undefined) {
113
+ const refusal = refuseLiveCopyWrite(docId, refusePath);
114
+ if (refusal)
115
+ await refusal;
116
+ }
78
117
  return await fn();
79
118
  }
80
119
  finally {
81
120
  release();
82
- if (_writeLocks.get(docId) === chained) {
83
- _writeLocks.delete(docId);
84
- }
121
+ const pending = (_writesPending.get(docId) ?? 1) - 1;
122
+ if (pending > 0)
123
+ _writesPending.set(docId, pending);
124
+ else
125
+ _writesPending.delete(docId);
85
126
  }
86
127
  }
87
128
  export async function withPreparedYDocMutation(docId, requestSource, run) {
@@ -133,7 +174,7 @@ export async function withPreparedYDocMutation(docId, requestSource, run) {
133
174
  candidate.destroy();
134
175
  throw error;
135
176
  }
136
- });
177
+ }, "withPreparedYDocMutation");
137
178
  }
138
179
  function buildStateToStore(doc, storedByteCount) {
139
180
  const encoded = Y.encodeStateAsUpdate(doc);
@@ -244,7 +285,7 @@ async function loadDoc(docId) {
244
285
  _loadLocks.delete(docId);
245
286
  }
246
287
  }
247
- export async function getDocForWrite(docId) {
288
+ async function getDocForWrite(docId) {
248
289
  const cached = _cache.get(docId);
249
290
  if (cached) {
250
291
  cached.lastAccess = Date.now();
@@ -257,42 +298,360 @@ async function awaitPendingWrite(docId) {
257
298
  if (pending)
258
299
  await pending.catch(() => { });
259
300
  }
260
- export async function getDoc(docId) {
301
+ async function readTarget(docId) {
302
+ // A live copy is read from SQL, never from a write in flight here, so it
303
+ // skips the write lock: a binding's persist runs under that lock and may
304
+ // read its own document.
305
+ if (liveCopiesDefined()) {
306
+ const binding = await liveCopyBindingFor(docId);
307
+ if (binding)
308
+ return { kind: "live", binding };
309
+ }
261
310
  await awaitPendingWrite(docId);
311
+ // Looked up again: the write waited for may have made it a live copy.
312
+ const binding = await liveCopyBindingFor(docId);
313
+ if (binding)
314
+ return { kind: "live", binding };
262
315
  const cached = _cache.get(docId);
263
316
  if (cached) {
264
317
  cached.lastAccess = Date.now();
265
318
  await mergeNewerStoredState(docId, cached);
266
- return cached.doc;
319
+ return { kind: "doc", doc: cached.doc };
267
320
  }
268
- return loadDoc(docId);
321
+ return { kind: "doc", doc: await loadDoc(docId) };
322
+ }
323
+ /** The cached document, for callers that only read it. */
324
+ async function readDoc(docId, path) {
325
+ const target = await readTarget(docId);
326
+ if (target.kind === "doc")
327
+ return target.doc;
328
+ throw new LiveCopyError("read_refused", docId, `Document ${docId} is a live copy, which is never decoded on the request thread, so ${path} cannot read it; use readLiveCopy or getState.`, { reason: path });
269
329
  }
330
+ /** Refuses live copies with `read_refused`; read those with `readLiveCopy`. */
331
+ export async function getDoc(docId) {
332
+ return readDoc(docId, "getDoc");
333
+ }
334
+ /**
335
+ * Commits a client update. Once any live copy is defined, the result says
336
+ * whether the app's rows were derived from it; with none defined it is
337
+ * undefined, as before live copies existed.
338
+ */
270
339
  export async function applyUpdate(docId, update, requestSource) {
271
340
  return withDocWriteLock(docId, async () => {
341
+ const liveCopy = await liveCopyBindingFor(docId);
342
+ if (liveCopy) {
343
+ return commitLiveCopyUpdate(liveCopy, docId, update, requestSource);
344
+ }
272
345
  const doc = await getDocForWrite(docId);
273
346
  Y.applyUpdate(doc, update);
274
347
  await persistMergedState(docId, doc, () => doc.getText(DEFAULT_FIELD).toString());
275
348
  emitCollabUpdate(docId, uint8ArrayToBase64(update), requestSource);
349
+ return liveCopiesDefined()
350
+ ? { liveCopy: "not_derived", liveCopyReason: "not_live_copy" }
351
+ : undefined;
276
352
  });
277
353
  }
278
- function isEmptyXmlSeedTarget(fragment) {
279
- if (fragment.length === 0)
280
- return true;
281
- if (fragment.length !== 1)
282
- return false;
283
- const first = fragment.get(0);
284
- if (!(first instanceof Y.XmlElement) || first.nodeName !== "paragraph") {
285
- return false;
354
+ const LIVE_COPY_COMMIT_ATTEMPTS = 5;
355
+ // An empty Yjs update: no structs, no deletions.
356
+ const EMPTY_STATE = new Uint8Array([0, 0]);
357
+ const EMPTY_CATCH_UP = {
358
+ stateVector: new Uint8Array([0]),
359
+ deletions: EMPTY_STATE,
360
+ };
361
+ // Live copies are cached as validated bytes, never as a Y.Doc.
362
+ const _liveCopyCache = new Map();
363
+ /** Caches the bytes `stored` holds; null caches an absent copy. */
364
+ async function cacheLiveCopyState(binding, docId, stored, catchUp, text) {
365
+ const entry = {
366
+ binding,
367
+ state: stored && stored.state.length > 0 ? stored.state : EMPTY_STATE,
368
+ catchUp,
369
+ stamp: stored && {
370
+ version: stored.version,
371
+ digest: await collabStateDigest(stored.state),
372
+ },
373
+ text,
374
+ lastAccess: Date.now(),
375
+ };
376
+ const stale = _cache.get(docId);
377
+ if (stale) {
378
+ stale.doc.destroy();
379
+ _cache.delete(docId);
286
380
  }
287
- for (let index = 0; index < first.length; index += 1) {
288
- const child = first.get(index);
289
- if (!(child instanceof Y.XmlText) || child.toString().length > 0) {
290
- return false;
381
+ _liveCopyCache.set(docId, entry);
382
+ evictLiveCopyCache();
383
+ return entry;
384
+ }
385
+ function evictLiveCopyCache() {
386
+ if (_liveCopyCache.size <= MAX_CACHE)
387
+ return;
388
+ let oldest = null;
389
+ let oldestTime = Infinity;
390
+ for (const [id, entry] of _liveCopyCache) {
391
+ if (entry.lastAccess < oldestTime) {
392
+ oldestTime = entry.lastAccess;
393
+ oldest = id;
291
394
  }
292
395
  }
293
- return true;
396
+ if (oldest)
397
+ _liveCopyCache.delete(oldest);
294
398
  }
295
- export async function seedXmlFragmentIfEmpty(docId, seedUpdate, requestSource) {
399
+ function servedState(state) {
400
+ return state.length > 0 ? state.slice() : EMPTY_STATE.slice();
401
+ }
402
+ /**
403
+ * The cached entry while the row still holds the bytes it was cached from.
404
+ * The digest matters as much as the version: a row deleted and inserted
405
+ * again, by any instance, starts back at version 0.
406
+ */
407
+ function cachedLiveCopy(binding, docId, stamp) {
408
+ const cached = _liveCopyCache.get(docId);
409
+ if (cached?.binding !== binding || !sameStamp(cached.stamp, stamp)) {
410
+ return undefined;
411
+ }
412
+ cached.lastAccess = Date.now();
413
+ return cached;
414
+ }
415
+ /** Cache misses being filled, shared by readers that saw the same row. */
416
+ const _liveCopyLoads = new Map();
417
+ /**
418
+ * The stored copy to change. When this instance cached the stored row, its
419
+ * bytes are used instead of re-reading it; the tasks get a copy, so a
420
+ * refused or conflicting attempt never touches the cache.
421
+ */
422
+ async function loadLiveCopyState(binding, docId) {
423
+ const cached = cachedLiveCopy(binding, docId, await loadYDocStamp(docId));
424
+ if (cached) {
425
+ return cached.stamp === null
426
+ ? { kind: "absent" }
427
+ : { kind: "stored", state: cached.state, ...cached.stamp };
428
+ }
429
+ return readStoredLiveCopy(binding, getDbExec(), docId);
430
+ }
431
+ function stampOf(stored) {
432
+ return stored.kind === "absent"
433
+ ? null
434
+ : { version: stored.version, digest: stored.digest };
435
+ }
436
+ /**
437
+ * The bytes a live copy serves to editors. A version this instance has not
438
+ * seen is checked in the runner first, so malformed state fails here instead
439
+ * of reaching an editor as an empty or broken document.
440
+ */
441
+ async function liveCopyStateForRead(binding, docId) {
442
+ await ensureTable();
443
+ const stamp = await loadYDocStamp(docId);
444
+ const cached = cachedLiveCopy(binding, docId, stamp);
445
+ if (cached)
446
+ return cached;
447
+ // One runner task per row, however many editors open it at once; the
448
+ // load reads the row again, so a joiner never gets older bytes than it saw.
449
+ const pending = _liveCopyLoads.get(docId);
450
+ if (pending?.binding === binding && sameStamp(pending.stamp, stamp)) {
451
+ return pending.load;
452
+ }
453
+ const load = loadLiveCopyForRead(binding, docId).finally(() => {
454
+ if (_liveCopyLoads.get(docId)?.load === load)
455
+ _liveCopyLoads.delete(docId);
456
+ });
457
+ _liveCopyLoads.set(docId, { binding, stamp, load });
458
+ return load;
459
+ }
460
+ async function loadLiveCopyForRead(binding, docId) {
461
+ const stored = await readStoredLiveCopy(binding, getDbExec(), docId);
462
+ if (stored.kind === "absent") {
463
+ return cacheLiveCopyState(binding, docId, null, EMPTY_CATCH_UP, null);
464
+ }
465
+ const read = await readLiveCopyState(binding, docId, stored.state, true);
466
+ return cacheLiveCopyState(binding, docId, stored, read.catchUp, read.kind === "live" ? read.text : null);
467
+ }
468
+ async function commitLiveCopyUpdate(binding, docId, update, requestSource) {
469
+ assertOwnTransaction(docId, "applyUpdate");
470
+ await ensureTable();
471
+ for (let attempt = 0; attempt < LIVE_COPY_COMMIT_ATTEMPTS; attempt += 1) {
472
+ const stored = await loadLiveCopyState(binding, docId);
473
+ const result = await applyLiveCopyUpdate(binding, docId, stored.kind === "absent" ? null : stored.state, update);
474
+ if (result.kind === "unchanged")
475
+ return { liveCopy: "unchanged" };
476
+ const previousVersion = stored.kind === "absent" ? null : stored.version;
477
+ const version = previousVersion === null ? 0 : previousVersion + 1;
478
+ const outcome = await commitLiveCopy(binding, {
479
+ docId,
480
+ origin: "update",
481
+ previousVersion,
482
+ version,
483
+ text: result.text,
484
+ state: result.state,
485
+ ...(requestSource ? { requestSource } : {}),
486
+ }, stampOf(stored));
487
+ if (!outcome)
488
+ continue;
489
+ await cacheLiveCopyState(binding, docId, { state: result.state, version }, result.catchUp, result.text);
490
+ emitCollabUpdate(docId, uint8ArrayToBase64(update), requestSource);
491
+ return outcome;
492
+ }
493
+ throw new LiveCopyError("version_conflict", docId, `Live copy ${docId} kept changing while the update was being committed; it was not saved.`, { reason: String(LIVE_COPY_COMMIT_ATTEMPTS) });
494
+ }
495
+ /**
496
+ * The stored live copy and derive(state). Absent (no row), unseeded (a row
497
+ * with nothing in the field, including a zero-byte state) and malformed
498
+ * (throws `malformed_state`) are never conflated.
499
+ */
500
+ export async function readLiveCopy(binding, docId) {
501
+ await assertLiveCopy(binding, docId);
502
+ const { stamp, state, text } = await liveCopyStateForRead(binding, docId);
503
+ if (stamp === null)
504
+ return { kind: "absent" };
505
+ if (text === null) {
506
+ return {
507
+ kind: "unseeded",
508
+ version: stamp.version,
509
+ state: servedState(state),
510
+ };
511
+ }
512
+ return {
513
+ kind: "live",
514
+ version: stamp.version,
515
+ state: servedState(state),
516
+ text,
517
+ };
518
+ }
519
+ /**
520
+ * Applies a named edit to a seeded live copy and commits it with its derived
521
+ * text, retrying on `_collab_docs.version` conflicts. Throws `not_seeded`
522
+ * for an absent or unseeded copy; seed it with `seedLiveCopy` first.
523
+ */
524
+ export async function mutateLiveCopy(binding, docId, options) {
525
+ const maxAttempts = options.maxAttempts ?? LIVE_COPY_COMMIT_ATTEMPTS;
526
+ if (!Number.isSafeInteger(maxAttempts) || maxAttempts < 1) {
527
+ throw new RangeError("mutateLiveCopy maxAttempts must be a positive integer");
528
+ }
529
+ if (typeof options.edit !== "string") {
530
+ throw new TypeError("mutateLiveCopy edit must name an edit in the runner's module");
531
+ }
532
+ const callPersist = options.persist;
533
+ if (callPersist !== undefined && typeof callPersist !== "function") {
534
+ throw new TypeError("mutateLiveCopy persist must be a function");
535
+ }
536
+ // Refused before joining the line: a caller's transaction waiting in it
537
+ // could hold rows the write ahead needs to commit.
538
+ assertOwnTransaction(docId, "mutateLiveCopy");
539
+ return withDocWriteLock(docId, async () => {
540
+ // In line, so a slow appliesTo cannot reorder writes or unbound them.
541
+ await assertLiveCopy(binding, docId);
542
+ await ensureTable();
543
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
544
+ const stored = await loadLiveCopyState(binding, docId);
545
+ if (stored.kind === "absent") {
546
+ throw new LiveCopyError("not_seeded", docId, `Live copy ${docId} is absent; seed it before editing.`, { reason: "absent" });
547
+ }
548
+ const result = await editLiveCopyState(binding, docId, stored.state, options.edit, options.input);
549
+ if (result.kind === "unchanged") {
550
+ if (callPersist &&
551
+ !(await persistLiveCopyCall(binding, docId, { version: stored.version, digest: stored.digest }, (tx) => callPersist(tx, {
552
+ changed: false,
553
+ version: stored.version,
554
+ text: result.text,
555
+ })))) {
556
+ continue;
557
+ }
558
+ return {
559
+ changed: false,
560
+ liveCopy: "unchanged",
561
+ version: stored.version,
562
+ state: servedState(stored.state),
563
+ text: result.text,
564
+ update: new Uint8Array(0),
565
+ attempts: attempt,
566
+ };
567
+ }
568
+ const version = stored.version + 1;
569
+ const outcome = await commitLiveCopy(binding, {
570
+ docId,
571
+ origin: "mutation",
572
+ previousVersion: stored.version,
573
+ version,
574
+ text: result.text,
575
+ state: result.state,
576
+ ...(options.requestSource
577
+ ? { requestSource: options.requestSource }
578
+ : {}),
579
+ }, stampOf(stored), callPersist &&
580
+ ((tx) => callPersist(tx, { changed: true, version, text: result.text })));
581
+ if (!outcome)
582
+ continue;
583
+ await cacheLiveCopyState(binding, docId, { state: result.state, version }, result.catchUp, result.text);
584
+ emitCollabUpdate(docId, uint8ArrayToBase64(result.update), options.requestSource);
585
+ touchAgentPresence(docId, options.requestSource, {
586
+ descriptor: { kind: "doc" },
587
+ });
588
+ return {
589
+ ...outcome,
590
+ changed: true,
591
+ version,
592
+ state: result.state.slice(),
593
+ text: result.text,
594
+ update: result.update,
595
+ attempts: attempt,
596
+ };
597
+ }
598
+ throw new LiveCopyError("version_conflict", docId, `Live copy ${docId} kept changing during ${maxAttempts} edit attempts; the edit was not saved.`, { reason: String(maxAttempts) });
599
+ });
600
+ }
601
+ /**
602
+ * Seeds an absent or unseeded live copy from `source`. A live copy that
603
+ * already has content comes back unchanged with `seeded: false`; malformed
604
+ * stored state throws `malformed_state` and is never overwritten.
605
+ */
606
+ export async function seedLiveCopy(binding, docId, options) {
607
+ assertOwnTransaction(docId, "seedLiveCopy");
608
+ return withDocWriteLock(docId, async () => {
609
+ await assertLiveCopy(binding, docId);
610
+ await ensureTable();
611
+ for (let attempt = 0; attempt < LIVE_COPY_COMMIT_ATTEMPTS; attempt += 1) {
612
+ const stored = await loadLiveCopyState(binding, docId);
613
+ const result = await seedLiveCopyState(binding, docId, stored.kind === "absent" ? null : stored.state, options.source);
614
+ if (result.kind === "live") {
615
+ if (stored.kind === "absent") {
616
+ throw new LiveCopyError("build_failed", docId, `The seed task for ${docId} called an absent copy live.`, { reason: "invalid_output" });
617
+ }
618
+ return {
619
+ seeded: false,
620
+ liveCopy: "unchanged",
621
+ version: stored.version,
622
+ state: servedState(stored.state),
623
+ text: result.text,
624
+ };
625
+ }
626
+ const previousVersion = stored.kind === "absent" ? null : stored.version;
627
+ const version = previousVersion === null ? 0 : previousVersion + 1;
628
+ const outcome = await commitLiveCopy(binding, {
629
+ docId,
630
+ origin: "seed",
631
+ previousVersion,
632
+ version,
633
+ text: result.text,
634
+ state: result.state,
635
+ ...(options.requestSource
636
+ ? { requestSource: options.requestSource }
637
+ : {}),
638
+ }, stampOf(stored));
639
+ if (!outcome)
640
+ continue;
641
+ await cacheLiveCopyState(binding, docId, { state: result.state, version }, result.catchUp, result.text);
642
+ emitCollabUpdate(docId, uint8ArrayToBase64(result.update), options.requestSource);
643
+ return {
644
+ ...outcome,
645
+ seeded: true,
646
+ version,
647
+ state: result.state.slice(),
648
+ text: result.text,
649
+ };
650
+ }
651
+ throw new LiveCopyError("version_conflict", docId, `Live copy ${docId} kept changing while its seed was being committed.`, { reason: String(LIVE_COPY_COMMIT_ATTEMPTS) });
652
+ });
653
+ }
654
+ function assertXmlFragmentSeed(seedUpdate) {
296
655
  const candidate = new Y.Doc();
297
656
  try {
298
657
  const decoded = Y.decodeUpdate(seedUpdate);
@@ -310,7 +669,33 @@ export async function seedXmlFragmentIfEmpty(docId, seedUpdate, requestSource) {
310
669
  finally {
311
670
  candidate.destroy();
312
671
  }
672
+ }
673
+ function isEmptyXmlSeedTarget(fragment) {
674
+ if (fragment.length === 0)
675
+ return true;
676
+ if (fragment.length !== 1)
677
+ return false;
678
+ const first = fragment.get(0);
679
+ if (!(first instanceof Y.XmlElement) || first.nodeName !== "paragraph") {
680
+ return false;
681
+ }
682
+ for (let index = 0; index < first.length; index += 1) {
683
+ const child = first.get(index);
684
+ if (!(child instanceof Y.XmlText) || child.toString().length > 0) {
685
+ return false;
686
+ }
687
+ }
688
+ return true;
689
+ }
690
+ export async function seedXmlFragmentIfEmpty(docId, seedUpdate, requestSource) {
691
+ // Once a live copy exists, decode only after the lock's refusal check, so
692
+ // a covered document never integrates a seed on this thread.
693
+ const validateInLock = liveCopiesDefined();
694
+ if (!validateInLock)
695
+ assertXmlFragmentSeed(seedUpdate);
313
696
  return withDocWriteLock(docId, async () => {
697
+ if (validateInLock)
698
+ assertXmlFragmentSeed(seedUpdate);
314
699
  for (let attempt = 0; attempt < 5; attempt += 1) {
315
700
  const stored = await loadYDocRecord(docId);
316
701
  const committed = new Y.Doc();
@@ -344,7 +729,7 @@ export async function seedXmlFragmentIfEmpty(docId, seedUpdate, requestSource) {
344
729
  }
345
730
  }
346
731
  throw new CollabBaseVersionConflictError(`Document ${docId} kept changing while its initial XmlFragment seed was being committed.`);
347
- });
732
+ }, "seedXmlFragmentIfEmpty");
348
733
  }
349
734
  export async function applyText(docId, newText, fieldName = DEFAULT_FIELD, requestSource, options = {}) {
350
735
  return withDocWriteLock(docId, async () => {
@@ -381,7 +766,7 @@ export async function applyText(docId, newText, fieldName = DEFAULT_FIELD, reque
381
766
  descriptor: computeTextEditDescriptor(oldText, newText),
382
767
  });
383
768
  return doc.getText(fieldName).toString();
384
- });
769
+ }, "applyText");
385
770
  }
386
771
  export async function searchAndReplace(docId, find, replace, requestSource) {
387
772
  return withDocWriteLock(docId, async () => {
@@ -409,19 +794,33 @@ export async function searchAndReplace(docId, find, replace, requestSource) {
409
794
  },
410
795
  });
411
796
  return { found: true, update };
412
- });
797
+ }, "searchAndReplace");
413
798
  }
414
799
  export async function getText(docId, fieldName = DEFAULT_FIELD) {
415
- const doc = await getDoc(docId);
800
+ const doc = await readDoc(docId, "getText");
416
801
  return doc.getText(fieldName).toString();
417
802
  }
418
803
  export async function getState(docId) {
419
- const doc = await getDoc(docId);
420
- return Y.encodeStateAsUpdate(doc);
804
+ const target = await readTarget(docId);
805
+ if (target.kind === "doc")
806
+ return Y.encodeStateAsUpdate(target.doc);
807
+ return (await liveCopyStateForRead(target.binding, docId)).state.slice();
421
808
  }
422
809
  export async function getIncUpdate(docId, clientStateVector) {
423
- const doc = await getDoc(docId);
424
- return Y.encodeStateAsUpdate(doc, clientStateVector);
810
+ const target = await readTarget(docId);
811
+ if (target.kind === "doc") {
812
+ return Y.encodeStateAsUpdate(target.doc, clientStateVector);
813
+ }
814
+ const { state, catchUp } = await liveCopyStateForRead(target.binding, docId);
815
+ // Diffing against the client's vector would decode the copy on this
816
+ // thread. A client at the same vector lacks at most deletions; any other
817
+ // gets the whole state, which Yjs applies idempotently.
818
+ return sameBytes(clientStateVector, catchUp.stateVector)
819
+ ? catchUp.deletions.slice()
820
+ : state.slice();
821
+ }
822
+ function sameBytes(a, b) {
823
+ return a.length === b.length && a.every((byte, index) => byte === b[index]);
425
824
  }
426
825
  export async function seedFromText(docId, text, fieldName = DEFAULT_FIELD, client) {
427
826
  return withDocWriteLock(docId, async () => {
@@ -458,7 +857,7 @@ export async function seedFromText(docId, text, fieldName = DEFAULT_FIELD, clien
458
857
  lastAccess: Date.now(),
459
858
  syncedVersion: existing ? existing.version + 1 : 0,
460
859
  });
461
- });
860
+ }, "seedFromText");
462
861
  }
463
862
  export async function applyJson(docId, newJson, fieldName = "data", _type = "map", requestSource) {
464
863
  return withDocWriteLock(docId, async () => {
@@ -469,7 +868,7 @@ export async function applyJson(docId, newJson, fieldName = "data", _type = "map
469
868
  await persistMergedState(docId, doc, () => JSON.stringify(yDocToJson(doc, fieldName)));
470
869
  emitCollabUpdate(docId, uint8ArrayToBase64(update), requestSource);
471
870
  touchAgentPresence(docId, requestSource, { descriptor: { kind: "doc" } });
472
- });
871
+ }, "applyJson");
473
872
  }
474
873
  export async function applyPatchOps(docId, ops, fieldName = "data", requestSource) {
475
874
  return withDocWriteLock(docId, async () => {
@@ -485,10 +884,10 @@ export async function applyPatchOps(docId, ops, fieldName = "data", requestSourc
485
884
  paths: ops.slice(0, 5).map((op) => op.path),
486
885
  },
487
886
  });
488
- });
887
+ }, "applyPatchOps");
489
888
  }
490
889
  export async function getJson(docId, fieldName = "data") {
491
- const doc = await getDoc(docId);
890
+ const doc = await readDoc(docId, "getJson");
492
891
  return yDocToJson(doc, fieldName);
493
892
  }
494
893
  export async function seedFromJson(docId, json, fieldName = "data", type = "map", client) {
@@ -526,9 +925,19 @@ export async function seedFromJson(docId, json, fieldName = "data", type = "map"
526
925
  lastAccess: Date.now(),
527
926
  syncedVersion: existing ? existing.version + 1 : 0,
528
927
  });
529
- });
928
+ }, "seedFromJson");
929
+ }
930
+ /** Overwrites a document's stored state and text. Refuses live copies. */
931
+ export function saveYDocState(docId, state, textSnapshot) {
932
+ if (!liveCopiesDefined()) {
933
+ return saveStoredYDocState(docId, state, textSnapshot);
934
+ }
935
+ // Refused under the lock: checked outside it, a seed that makes the
936
+ // document a live copy could land between the check and this overwrite.
937
+ return withDocWriteLock(docId, () => saveStoredYDocState(docId, state, textSnapshot), "saveYDocState");
530
938
  }
531
939
  export function releaseDoc(docId) {
940
+ _liveCopyCache.delete(docId);
532
941
  const entry = _cache.get(docId);
533
942
  if (entry) {
534
943
  entry.doc.destroy();
@@ -165,5 +165,11 @@ export declare function createDbExec(config?: DbExecConfig): Promise<DbExec>;
165
165
  export declare function annotateMissingTable(err: unknown, sql: unknown): unknown;
166
166
  export declare function withDbExec<T>(exec: DbExec, run: () => T): T;
167
167
  export declare function getScopedDbExec(): DbExec | undefined;
168
+ /**
169
+ * True while a `getDbExec().transaction()` callback runs. That callback leaves
170
+ * `getScopedDbExec()` unset, so code that must not run inside a caller's
171
+ * transaction asks this as well.
172
+ */
173
+ export declare function insideDbTransactionCallback(): boolean;
168
174
  export declare function getDbExec(): DbExec;
169
175
  export declare function closeDbExec(): Promise<void>;