@agent-native/core 0.221.0 → 0.221.1-nightly-20261028032829
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/client/agent-chat.js +6 -1
- package/dist/client/api-path.d.ts +2 -0
- package/dist/client/api-path.js +76 -55
- package/dist/client/application-state.js +12 -6
- package/dist/client/browser-tab-id.d.ts +2 -0
- package/dist/client/browser-tab-id.js +113 -13
- package/dist/client/collab/index.d.ts +1 -1
- package/dist/client/embed-auth.js +145 -26
- package/dist/client/route-state.js +15 -4
- package/dist/client/sse-event-processor.js +3 -0
- package/dist/client/use-change-version.js +6 -1
- package/dist/client/use-db-sync.js +70 -11
- package/dist/collab/client.d.ts +34 -0
- package/dist/collab/client.js +506 -53
- package/dist/deploy/build.js +10 -0
- package/dist/mcp/embed-app.js +165 -16
- package/dist/provider-api/actions/custom-provider-registration.d.ts +3 -3
- package/dist/provider-api/actions/provider-api.d.ts +4 -4
- package/dist/server/app-origin-config.d.ts +2 -0
- package/dist/server/app-origin-config.js +10 -0
- package/dist/shared/mcp-app-widget-embed.d.ts +23 -1
- package/dist/shared/mcp-app-widget-embed.js +178 -5
- package/dist/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +41 -18
- package/package.json +4 -4
- package/src/templates/factory/.agents/skills/review-latest-feedback/SKILL.md +41 -18
package/dist/collab/client.js
CHANGED
|
@@ -113,10 +113,63 @@ const EMPTY_SNAPSHOT = Object.freeze({
|
|
|
113
113
|
isLoading: false,
|
|
114
114
|
isSynced: false,
|
|
115
115
|
initialization: { status: "loading" },
|
|
116
|
+
durableUpdatesFailed: false,
|
|
117
|
+
updatesRefused: false,
|
|
116
118
|
activeUsers: [],
|
|
117
119
|
agentActive: false,
|
|
118
120
|
agentPresent: false,
|
|
119
121
|
});
|
|
122
|
+
function readUpdateAck(body) {
|
|
123
|
+
if (!body || typeof body !== "object") {
|
|
124
|
+
throw new Error("The collab update response was not an object.");
|
|
125
|
+
}
|
|
126
|
+
const { liveCopy, liveCopyReason } = body;
|
|
127
|
+
if (liveCopy === undefined)
|
|
128
|
+
return { liveCopy: null };
|
|
129
|
+
if (liveCopy !== "derived" &&
|
|
130
|
+
liveCopy !== "not_derived" &&
|
|
131
|
+
liveCopy !== "unchanged") {
|
|
132
|
+
throw new Error(`Unknown live-copy outcome ${String(liveCopy)}.`);
|
|
133
|
+
}
|
|
134
|
+
return typeof liveCopyReason === "string"
|
|
135
|
+
? { liveCopy, liveCopyReason }
|
|
136
|
+
: { liveCopy };
|
|
137
|
+
}
|
|
138
|
+
// Unacknowledged updates kept in localStorage, one entry per connection. Yjs
|
|
139
|
+
// integrates an update once, so sending one the server already holds again
|
|
140
|
+
// changes nothing.
|
|
141
|
+
const OUTBOX_PREFIX = "agent-native-collab-outbox-v1:";
|
|
142
|
+
const VERSIONED_OUTBOX_PREFIX = "agent-native-collab-outbox-v2:";
|
|
143
|
+
const OUTBOX_ORIGIN = "collab-outbox";
|
|
144
|
+
const OUTBOX_OWNER_LOCK_PREFIX = "agent-native-collab-outbox-owner:";
|
|
145
|
+
function outboxPrefix(docKey, email, prefix = OUTBOX_PREFIX) {
|
|
146
|
+
return `${prefix}${encodeURIComponent(docKey)}:${encodeURIComponent(email)}:`;
|
|
147
|
+
}
|
|
148
|
+
function readStoredUpdates(entry, legacyRequireLiveCopyOutcome) {
|
|
149
|
+
if (entry.version === 1)
|
|
150
|
+
return [
|
|
151
|
+
{
|
|
152
|
+
update: base64ToUint8Array(entry.raw),
|
|
153
|
+
requireLiveCopyOutcome: legacyRequireLiveCopyOutcome,
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
const body = JSON.parse(entry.raw);
|
|
157
|
+
if (body?.version !== 2 ||
|
|
158
|
+
!Array.isArray(body.updates) ||
|
|
159
|
+
body.updates.length === 0 ||
|
|
160
|
+
body.updates.length > 2)
|
|
161
|
+
throw new Error("The collab outbox has an unreadable update list.");
|
|
162
|
+
return body.updates.map((value) => {
|
|
163
|
+
const item = value;
|
|
164
|
+
if (typeof item?.update !== "string" ||
|
|
165
|
+
typeof item.requireLiveCopyOutcome !== "boolean")
|
|
166
|
+
throw new Error("The collab outbox has an unreadable update.");
|
|
167
|
+
return {
|
|
168
|
+
update: base64ToUint8Array(item.update),
|
|
169
|
+
requireLiveCopyOutcome: item.requireLiveCopyOutcome,
|
|
170
|
+
};
|
|
171
|
+
});
|
|
172
|
+
}
|
|
120
173
|
const requestSyncUnavailable = () => Promise.resolve({ status: "unavailable" });
|
|
121
174
|
// Nothing is outstanding without a connection.
|
|
122
175
|
const flushUpdatesUnavailable = () => Promise.resolve(true);
|
|
@@ -136,13 +189,27 @@ class CollabDocConnection {
|
|
|
136
189
|
docMissing = false;
|
|
137
190
|
requestSource;
|
|
138
191
|
lastSetUser = null;
|
|
192
|
+
pageHidden = false;
|
|
193
|
+
presenceClear = null;
|
|
194
|
+
signedOut = false;
|
|
195
|
+
keepaliveSnapshots = new Set();
|
|
196
|
+
userGeneration = 0;
|
|
139
197
|
pendingUpdates = [];
|
|
198
|
+
deliveredUpdates = new WeakSet();
|
|
199
|
+
strictDeliveryVersion = 0;
|
|
140
200
|
flushTimer = null;
|
|
141
201
|
updateInFlight = false;
|
|
142
|
-
keepaliveInFlight = false;
|
|
143
202
|
updateErrors = 0;
|
|
203
|
+
updateBatchLimit = Infinity;
|
|
144
204
|
updateAbortController = null;
|
|
145
205
|
updateHandlerAttached = false;
|
|
206
|
+
keepsUnsentUpdates = false;
|
|
207
|
+
unsentUpdatesRestored = false;
|
|
208
|
+
onlineDuringUpdate = false;
|
|
209
|
+
releaseOutboxOwner = null;
|
|
210
|
+
outboxOwnerId = `${Date.now().toString(36)}-${Math.random()
|
|
211
|
+
.toString(36)
|
|
212
|
+
.slice(2)}`;
|
|
146
213
|
syncActive = false;
|
|
147
214
|
pollTimer = null;
|
|
148
215
|
pollGeneration = 0;
|
|
@@ -176,6 +243,8 @@ class CollabDocConnection {
|
|
|
176
243
|
isLoading: true,
|
|
177
244
|
isSynced: false,
|
|
178
245
|
initialization: { status: "loading" },
|
|
246
|
+
durableUpdatesFailed: false,
|
|
247
|
+
updatesRefused: false,
|
|
179
248
|
activeUsers: [],
|
|
180
249
|
agentActive: false,
|
|
181
250
|
agentPresent: false,
|
|
@@ -188,7 +257,7 @@ class CollabDocConnection {
|
|
|
188
257
|
return collabRegistryKey(this.docId, this.baseUrl, this.actorIdentity);
|
|
189
258
|
}
|
|
190
259
|
get retiredUpdatesKey() {
|
|
191
|
-
const email = this.lastSetUser?.email
|
|
260
|
+
const email = this.lastSetUser?.email.trim().toLowerCase();
|
|
192
261
|
return email
|
|
193
262
|
? `${this.registryKey}\0${email}\0${this.requestSource ?? ""}`
|
|
194
263
|
: null;
|
|
@@ -215,7 +284,7 @@ class CollabDocConnection {
|
|
|
215
284
|
collabConnectionRegistry.set(nextKey, this);
|
|
216
285
|
return true;
|
|
217
286
|
}
|
|
218
|
-
add(id, sub) {
|
|
287
|
+
add(id, sub, user) {
|
|
219
288
|
this.subscribers.set(id, sub);
|
|
220
289
|
if (this.disposeTimer) {
|
|
221
290
|
clearTimeout(this.disposeTimer);
|
|
@@ -225,6 +294,15 @@ class CollabDocConnection {
|
|
|
225
294
|
this.requestSource = sub.requestSource;
|
|
226
295
|
registerOwnCollabSource(sub.requestSource);
|
|
227
296
|
}
|
|
297
|
+
if (user)
|
|
298
|
+
this.setUser(user);
|
|
299
|
+
// Once kept, updates stay kept until acknowledged, even if the caller
|
|
300
|
+
// stops asking.
|
|
301
|
+
if (sub.durableUpdates && !this.keepsUnsentUpdates) {
|
|
302
|
+
this.keepsUnsentUpdates = true;
|
|
303
|
+
this.storeUnsentUpdates();
|
|
304
|
+
this.restoreUnsentUpdates();
|
|
305
|
+
}
|
|
228
306
|
if (!this.started) {
|
|
229
307
|
this.started = true;
|
|
230
308
|
this.start();
|
|
@@ -243,6 +321,10 @@ class CollabDocConnection {
|
|
|
243
321
|
remove(id) {
|
|
244
322
|
this.subscribers.delete(id);
|
|
245
323
|
if (this.subscribers.size === 0) {
|
|
324
|
+
if (this.signedOut) {
|
|
325
|
+
this.dispose();
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
246
328
|
cancelAwarenessPush(this.baseUrl, this.docId, this.ydoc.clientID);
|
|
247
329
|
// A lingering connection may outlive its document (a deleted Screen).
|
|
248
330
|
this.stopSync();
|
|
@@ -287,14 +369,16 @@ class CollabDocConnection {
|
|
|
287
369
|
}
|
|
288
370
|
void this.flushPendingUpdates(true);
|
|
289
371
|
this.detachUpdateHandler();
|
|
372
|
+
this.releaseOutboxOwner?.();
|
|
373
|
+
this.releaseOutboxOwner = null;
|
|
290
374
|
if (this.agentTimer) {
|
|
291
375
|
clearTimeout(this.agentTimer);
|
|
292
376
|
this.agentTimer = null;
|
|
293
377
|
}
|
|
294
378
|
this.awareness.off("change", this.handleAwarenessChange);
|
|
295
|
-
|
|
296
|
-
if (
|
|
297
|
-
void
|
|
379
|
+
const clear = this.clearPresence();
|
|
380
|
+
if (clear) {
|
|
381
|
+
void clear
|
|
298
382
|
.catch(() => { })
|
|
299
383
|
.finally(() => {
|
|
300
384
|
forgetAwarenessClientSequence(this.baseUrl, this.docId, this.ydoc.clientID);
|
|
@@ -312,21 +396,35 @@ class CollabDocConnection {
|
|
|
312
396
|
async clearPresenceBeforeSignOut() {
|
|
313
397
|
if (this.disposed)
|
|
314
398
|
return;
|
|
399
|
+
this.signedOut = true;
|
|
315
400
|
this.stopSync();
|
|
316
|
-
|
|
317
|
-
if (!this.awareness.getLocalState())
|
|
318
|
-
return;
|
|
401
|
+
const clear = this.clearPresence();
|
|
319
402
|
this.awareness.setLocalState(null);
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (!response.ok) {
|
|
323
|
-
throw new Error(`Awareness clear returned HTTP ${response.status}`);
|
|
324
|
-
}
|
|
403
|
+
if (collabConnectionRegistry.get(this.registryKey) === this) {
|
|
404
|
+
collabConnectionRegistry.delete(this.registryKey);
|
|
325
405
|
}
|
|
326
|
-
|
|
327
|
-
|
|
406
|
+
if (this.subscribers.size === 0)
|
|
407
|
+
this.dispose();
|
|
408
|
+
if (!clear)
|
|
409
|
+
return;
|
|
410
|
+
const response = await clear;
|
|
411
|
+
if (!response.ok) {
|
|
412
|
+
throw new Error(`Awareness clear returned HTTP ${response.status}`);
|
|
328
413
|
}
|
|
329
414
|
}
|
|
415
|
+
clearPresence() {
|
|
416
|
+
cancelAwarenessPush(this.baseUrl, this.docId, this.ydoc.clientID);
|
|
417
|
+
if (!this.presenceClear && this.awareness.getLocalState()) {
|
|
418
|
+
this.presenceClear = postAwarenessState(this.baseUrl, this.docId, this.ydoc.clientID, null, true);
|
|
419
|
+
}
|
|
420
|
+
return this.presenceClear;
|
|
421
|
+
}
|
|
422
|
+
get canPublishPresence() {
|
|
423
|
+
return (!this.disposed &&
|
|
424
|
+
!this.signedOut &&
|
|
425
|
+
!this.pageHidden &&
|
|
426
|
+
this.presenceClear === null);
|
|
427
|
+
}
|
|
330
428
|
get effectivePollInterval() {
|
|
331
429
|
let min = Infinity;
|
|
332
430
|
for (const sub of this.subscribers.values()) {
|
|
@@ -356,6 +454,179 @@ class CollabDocConnection {
|
|
|
356
454
|
sub.onSnapshot(this.snapshot);
|
|
357
455
|
}
|
|
358
456
|
}
|
|
457
|
+
get hasStrictSubscriber() {
|
|
458
|
+
return Array.from(this.subscribers.values()).some((sub) => sub.requireLiveCopyOutcome());
|
|
459
|
+
}
|
|
460
|
+
get outboxScope() {
|
|
461
|
+
const email = this.lastSetUser?.email.trim().toLowerCase();
|
|
462
|
+
if (!email || typeof window === "undefined")
|
|
463
|
+
return null;
|
|
464
|
+
const prefix = outboxPrefix(this.registryKey, email);
|
|
465
|
+
const versionedPrefix = outboxPrefix(this.registryKey, email, VERSIONED_OUTBOX_PREFIX);
|
|
466
|
+
return {
|
|
467
|
+
prefix,
|
|
468
|
+
key: prefix + this.outboxOwnerId,
|
|
469
|
+
versionedPrefix,
|
|
470
|
+
versionedKey: versionedPrefix + this.outboxOwnerId,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
markDurableUpdatesFailed() {
|
|
474
|
+
if (!this.snapshot.durableUpdatesFailed) {
|
|
475
|
+
this.setSnapshot({ durableUpdatesFailed: true });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
holdOutboxOwner(key) {
|
|
479
|
+
if (this.disposed || this.releaseOutboxOwner)
|
|
480
|
+
return;
|
|
481
|
+
const locks = typeof navigator === "undefined" ? undefined : navigator.locks;
|
|
482
|
+
if (typeof locks?.request !== "function") {
|
|
483
|
+
this.markDurableUpdatesFailed();
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
// Queue the owner lock before publishing bytes an adopting tab can read.
|
|
487
|
+
const held = new Promise((release) => {
|
|
488
|
+
this.releaseOutboxOwner = release;
|
|
489
|
+
});
|
|
490
|
+
void locks
|
|
491
|
+
.request(OUTBOX_OWNER_LOCK_PREFIX + key, () => held)
|
|
492
|
+
.catch(() => {
|
|
493
|
+
if (!this.disposed)
|
|
494
|
+
this.markDurableUpdatesFailed();
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
storeUnsentUpdates() {
|
|
498
|
+
if (!this.keepsUnsentUpdates)
|
|
499
|
+
return;
|
|
500
|
+
const scope = this.outboxScope;
|
|
501
|
+
if (!scope)
|
|
502
|
+
return;
|
|
503
|
+
this.holdOutboxOwner(scope.key);
|
|
504
|
+
try {
|
|
505
|
+
if (this.pendingUpdates.length === 0) {
|
|
506
|
+
window.localStorage.removeItem(scope.key);
|
|
507
|
+
window.localStorage.removeItem(scope.versionedKey);
|
|
508
|
+
}
|
|
509
|
+
else {
|
|
510
|
+
const encode = (items) => uint8ArrayToBase64(Y.mergeUpdates(items.map((item) => item.update)));
|
|
511
|
+
const updates = [true, false].flatMap((requireLiveCopyOutcome) => {
|
|
512
|
+
const items = this.pendingUpdates.filter((item) => item.requireLiveCopyOutcome === requireLiveCopyOutcome);
|
|
513
|
+
return items.length
|
|
514
|
+
? [{ update: encode(items), requireLiveCopyOutcome }]
|
|
515
|
+
: [];
|
|
516
|
+
});
|
|
517
|
+
// Older clients only understand v1's bare Yjs bytes and must not
|
|
518
|
+
// adopt or delete a versioned custody record.
|
|
519
|
+
window.localStorage.setItem(scope.versionedKey, JSON.stringify({ version: 2, updates }));
|
|
520
|
+
window.localStorage.removeItem(scope.key);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
catch {
|
|
524
|
+
this.markDurableUpdatesFailed();
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
/**
|
|
528
|
+
* Sends what other connections for this doc and user kept: a tab that
|
|
529
|
+
* closed or reloaded before its updates were acknowledged. Applying them
|
|
530
|
+
* queues what the doc lacks as this connection's own unsent updates, which
|
|
531
|
+
* are kept before the other entry is removed. A live or frozen tab keeps
|
|
532
|
+
* its owner lock, so another connection leaves its entry untouched.
|
|
533
|
+
*/
|
|
534
|
+
restoreUnsentUpdates() {
|
|
535
|
+
if (this.unsentUpdatesRestored ||
|
|
536
|
+
!this.keepsUnsentUpdates ||
|
|
537
|
+
!this.updateHandlerAttached ||
|
|
538
|
+
this.disposed) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const scope = this.outboxScope;
|
|
542
|
+
if (!scope)
|
|
543
|
+
return;
|
|
544
|
+
this.unsentUpdatesRestored = true;
|
|
545
|
+
const stored = [];
|
|
546
|
+
try {
|
|
547
|
+
const storage = window.localStorage;
|
|
548
|
+
for (let index = 0; index < storage.length; index += 1) {
|
|
549
|
+
const key = storage.key(index);
|
|
550
|
+
if (!key ||
|
|
551
|
+
(!key.startsWith(scope.prefix) &&
|
|
552
|
+
!key.startsWith(scope.versionedPrefix)) ||
|
|
553
|
+
key === scope.key ||
|
|
554
|
+
key === scope.versionedKey)
|
|
555
|
+
continue;
|
|
556
|
+
const raw = storage.getItem(key);
|
|
557
|
+
if (raw !== null)
|
|
558
|
+
stored.push({
|
|
559
|
+
key,
|
|
560
|
+
raw,
|
|
561
|
+
version: key.startsWith(scope.versionedPrefix) ? 2 : 1,
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
this.markDurableUpdatesFailed();
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
const locks = typeof navigator === "undefined" ? undefined : navigator.locks;
|
|
570
|
+
if (typeof locks?.request !== "function") {
|
|
571
|
+
this.markDurableUpdatesFailed();
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
for (const entry of stored) {
|
|
575
|
+
const ownerKey = entry.key.startsWith(scope.versionedPrefix)
|
|
576
|
+
? scope.prefix + entry.key.slice(scope.versionedPrefix.length)
|
|
577
|
+
: entry.key;
|
|
578
|
+
void locks
|
|
579
|
+
.request(OUTBOX_OWNER_LOCK_PREFIX + ownerKey, { ifAvailable: true }, (lock) => {
|
|
580
|
+
if (!lock ||
|
|
581
|
+
this.disposed ||
|
|
582
|
+
this.outboxScope?.prefix !== scope.prefix)
|
|
583
|
+
return;
|
|
584
|
+
try {
|
|
585
|
+
const raw = window.localStorage.getItem(entry.key);
|
|
586
|
+
if (raw !== null)
|
|
587
|
+
this.adoptStoredOutbox({ ...entry, raw });
|
|
588
|
+
}
|
|
589
|
+
catch {
|
|
590
|
+
this.markDurableUpdatesFailed();
|
|
591
|
+
}
|
|
592
|
+
})
|
|
593
|
+
.catch(() => {
|
|
594
|
+
if (!this.disposed)
|
|
595
|
+
this.markDurableUpdatesFailed();
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
adoptStoredOutbox(entry) {
|
|
600
|
+
try {
|
|
601
|
+
for (const item of readStoredUpdates(entry, this.hasStrictSubscriber)) {
|
|
602
|
+
if (entry.version === 2 || item.requireLiveCopyOutcome) {
|
|
603
|
+
Y.applyUpdate(this.ydoc, item.update, "remote");
|
|
604
|
+
// Applying one custody group can integrate another's pending
|
|
605
|
+
// dependencies without emitting a separate update event.
|
|
606
|
+
this.enqueueUpdate(item.update, item.requireLiveCopyOutcome);
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
Y.applyUpdate(this.ydoc, item.update, OUTBOX_ORIGIN);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
// Kept nowhere else while this connection cannot keep its own.
|
|
613
|
+
if (this.snapshot.durableUpdatesFailed)
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
// An entry that cannot be applied never will be; it is reported,
|
|
618
|
+
// then removed so it is not reported on every open.
|
|
619
|
+
this.markDurableUpdatesFailed();
|
|
620
|
+
}
|
|
621
|
+
try {
|
|
622
|
+
if (window.localStorage.getItem(entry.key) === entry.raw) {
|
|
623
|
+
window.localStorage.removeItem(entry.key);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
this.markDurableUpdatesFailed();
|
|
628
|
+
}
|
|
629
|
+
}
|
|
359
630
|
markAgentActive() {
|
|
360
631
|
this.setSnapshot({ agentActive: true });
|
|
361
632
|
if (this.agentTimer)
|
|
@@ -371,6 +642,8 @@ class CollabDocConnection {
|
|
|
371
642
|
if (!this.bindActorIdentity(user.email))
|
|
372
643
|
return;
|
|
373
644
|
const prev = this.lastSetUser;
|
|
645
|
+
if (prev?.email !== user.email)
|
|
646
|
+
this.userGeneration++;
|
|
374
647
|
if (prev && prev.email !== user.email) {
|
|
375
648
|
if (this.pendingUpdates.length && this.retiredUpdatesKey) {
|
|
376
649
|
retiredCollabUpdates.set(this.retiredUpdatesKey, this.pendingUpdates);
|
|
@@ -381,6 +654,12 @@ class CollabDocConnection {
|
|
|
381
654
|
clearTimeout(this.flushTimer);
|
|
382
655
|
this.flushTimer = null;
|
|
383
656
|
this.updateErrors = 0;
|
|
657
|
+
this.updateBatchLimit = Infinity;
|
|
658
|
+
if (this.snapshot.updatesRefused) {
|
|
659
|
+
this.setSnapshot({ updatesRefused: false });
|
|
660
|
+
}
|
|
661
|
+
// Kept updates are per account.
|
|
662
|
+
this.unsentUpdatesRestored = false;
|
|
384
663
|
}
|
|
385
664
|
if (prev &&
|
|
386
665
|
prev.name === user.name &&
|
|
@@ -406,11 +685,13 @@ class CollabDocConnection {
|
|
|
406
685
|
retiredCollabUpdates.delete(retiredKey);
|
|
407
686
|
this.pendingUpdates.push(...retired);
|
|
408
687
|
if (this.snapshot.initialization.status === "ready") {
|
|
409
|
-
for (const
|
|
410
|
-
Y.applyUpdate(this.ydoc, update, "remote");
|
|
688
|
+
for (const item of retired)
|
|
689
|
+
Y.applyUpdate(this.ydoc, item.update, "remote");
|
|
411
690
|
void this.flushPendingUpdates();
|
|
412
691
|
}
|
|
413
692
|
}
|
|
693
|
+
this.storeUnsentUpdates();
|
|
694
|
+
this.restoreUnsentUpdates();
|
|
414
695
|
this.awareness.setLocalStateField("user", {
|
|
415
696
|
name: user.name,
|
|
416
697
|
email: user.email,
|
|
@@ -449,7 +730,7 @@ class CollabDocConnection {
|
|
|
449
730
|
this.snapshot.isSynced &&
|
|
450
731
|
origin === "local" &&
|
|
451
732
|
this.syncActive &&
|
|
452
|
-
|
|
733
|
+
this.canPublishPresence) {
|
|
453
734
|
scheduleAwarenessPush(this.baseUrl, this.docId, this.ydoc.clientID, () => this.awareness.getLocalState());
|
|
454
735
|
}
|
|
455
736
|
};
|
|
@@ -528,8 +809,8 @@ class CollabDocConnection {
|
|
|
528
809
|
validationDoc.destroy();
|
|
529
810
|
}
|
|
530
811
|
Y.applyUpdate(this.ydoc, binary, "remote");
|
|
531
|
-
for (const
|
|
532
|
-
Y.applyUpdate(this.ydoc, update, "remote");
|
|
812
|
+
for (const item of this.pendingUpdates) {
|
|
813
|
+
Y.applyUpdate(this.ydoc, item.update, "remote");
|
|
533
814
|
}
|
|
534
815
|
}
|
|
535
816
|
catch {
|
|
@@ -543,6 +824,7 @@ class CollabDocConnection {
|
|
|
543
824
|
initialization: { status: "ready" },
|
|
544
825
|
});
|
|
545
826
|
this.startTransport();
|
|
827
|
+
this.restoreUnsentUpdates();
|
|
546
828
|
}, () => {
|
|
547
829
|
if (this.disposed)
|
|
548
830
|
return;
|
|
@@ -601,8 +883,13 @@ class CollabDocConnection {
|
|
|
601
883
|
clearTimeout(this.flushTimer);
|
|
602
884
|
this.flushTimer = null;
|
|
603
885
|
}
|
|
886
|
+
const strictDeliveryVersion = this.strictDeliveryVersion;
|
|
604
887
|
await this.flushPendingUpdates();
|
|
605
|
-
if (this.
|
|
888
|
+
if (this.snapshot.updatesRefused)
|
|
889
|
+
return false;
|
|
890
|
+
if (this.updateErrors > 0 &&
|
|
891
|
+
(strictDeliveryVersion === this.strictDeliveryVersion ||
|
|
892
|
+
!this.pendingUpdates.some((item) => !item.requireLiveCopyOutcome)))
|
|
606
893
|
return false;
|
|
607
894
|
}
|
|
608
895
|
return this.pendingUpdates.length === 0;
|
|
@@ -610,7 +897,11 @@ class CollabDocConnection {
|
|
|
610
897
|
handleDocUpdate = (update, origin) => {
|
|
611
898
|
if (origin === "remote")
|
|
612
899
|
return;
|
|
613
|
-
this.
|
|
900
|
+
this.enqueueUpdate(update, this.hasStrictSubscriber);
|
|
901
|
+
};
|
|
902
|
+
enqueueUpdate(update, requireLiveCopyOutcome) {
|
|
903
|
+
this.pendingUpdates.push({ update, requireLiveCopyOutcome });
|
|
904
|
+
this.storeUnsentUpdates();
|
|
614
905
|
if (this.updateErrors && this.flushTimer)
|
|
615
906
|
return;
|
|
616
907
|
if (this.flushTimer)
|
|
@@ -619,9 +910,35 @@ class CollabDocConnection {
|
|
|
619
910
|
this.flushTimer = null;
|
|
620
911
|
void this.flushPendingUpdates();
|
|
621
912
|
}, UPDATE_DEBOUNCE_MS);
|
|
622
|
-
}
|
|
913
|
+
}
|
|
623
914
|
handlePageHide = () => {
|
|
624
|
-
this.flushPendingUpdates(true);
|
|
915
|
+
void this.flushPendingUpdates(true);
|
|
916
|
+
this.pageHidden = true;
|
|
917
|
+
void this.clearPresence()?.catch(() => { });
|
|
918
|
+
};
|
|
919
|
+
handlePageShow = () => {
|
|
920
|
+
if (!this.pageHidden || this.disposed || this.signedOut)
|
|
921
|
+
return;
|
|
922
|
+
this.pageHidden = false;
|
|
923
|
+
// BFCache must retain the doc's sequence/token until it is retired.
|
|
924
|
+
this.presenceClear = null;
|
|
925
|
+
this.publishVisibility(!isDocumentHidden());
|
|
926
|
+
this.pollNow();
|
|
927
|
+
};
|
|
928
|
+
// A tab that comes back online sends what it kept without waiting out the
|
|
929
|
+
// retry backoff it built up while offline.
|
|
930
|
+
handleOnline = () => {
|
|
931
|
+
if (!this.keepsUnsentUpdates)
|
|
932
|
+
return;
|
|
933
|
+
if (this.updateInFlight) {
|
|
934
|
+
this.onlineDuringUpdate = true;
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
if (this.flushTimer) {
|
|
938
|
+
clearTimeout(this.flushTimer);
|
|
939
|
+
this.flushTimer = null;
|
|
940
|
+
}
|
|
941
|
+
void this.flushPendingUpdates();
|
|
625
942
|
};
|
|
626
943
|
attachUpdateHandler() {
|
|
627
944
|
if (this.updateHandlerAttached || this.docMissing)
|
|
@@ -630,6 +947,8 @@ class CollabDocConnection {
|
|
|
630
947
|
this.ydoc.on("update", this.handleDocUpdate);
|
|
631
948
|
if (typeof window !== "undefined") {
|
|
632
949
|
window.addEventListener("pagehide", this.handlePageHide);
|
|
950
|
+
window.addEventListener("online", this.handleOnline);
|
|
951
|
+
window.addEventListener("pageshow", this.handlePageShow);
|
|
633
952
|
}
|
|
634
953
|
}
|
|
635
954
|
detachUpdateHandler() {
|
|
@@ -639,14 +958,16 @@ class CollabDocConnection {
|
|
|
639
958
|
this.ydoc.off("update", this.handleDocUpdate);
|
|
640
959
|
if (typeof window !== "undefined") {
|
|
641
960
|
window.removeEventListener("pagehide", this.handlePageHide);
|
|
961
|
+
window.removeEventListener("online", this.handleOnline);
|
|
962
|
+
window.removeEventListener("pageshow", this.handlePageShow);
|
|
642
963
|
}
|
|
643
964
|
}
|
|
644
965
|
async flushPendingUpdates(keepalive = false) {
|
|
645
|
-
if (
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
966
|
+
if (this.snapshot.updatesRefused ||
|
|
967
|
+
(!keepalive &&
|
|
968
|
+
(this.disposed ||
|
|
969
|
+
this.updateInFlight ||
|
|
970
|
+
(this.updateErrors > 0 && this.flushTimer !== null))))
|
|
650
971
|
return;
|
|
651
972
|
if (this.flushTimer) {
|
|
652
973
|
clearTimeout(this.flushTimer);
|
|
@@ -654,17 +975,35 @@ class CollabDocConnection {
|
|
|
654
975
|
}
|
|
655
976
|
if (this.pendingUpdates.length === 0)
|
|
656
977
|
return;
|
|
657
|
-
const
|
|
978
|
+
const ordinary = this.pendingUpdates.filter((item) => !item.requireLiveCopyOutcome);
|
|
979
|
+
// Ordinary successors may depend on retained strict bytes. Transport
|
|
980
|
+
// acceptance does not release those bytes' outcome custody.
|
|
981
|
+
const predecessors = ordinary.length
|
|
982
|
+
? this.pendingUpdates.filter((item) => item.requireLiveCopyOutcome && !this.deliveredUpdates.has(item))
|
|
983
|
+
: [];
|
|
984
|
+
const toSend = (predecessors.length
|
|
985
|
+
? predecessors
|
|
986
|
+
: ordinary.length
|
|
987
|
+
? ordinary
|
|
988
|
+
: this.pendingUpdates).slice(0, this.updateBatchLimit);
|
|
989
|
+
if (keepalive &&
|
|
990
|
+
[...this.keepaliveSnapshots].some((snapshot) => toSend.every((item) => snapshot.includes(item))))
|
|
991
|
+
return;
|
|
992
|
+
if (keepalive)
|
|
993
|
+
this.keepaliveSnapshots.add(toSend);
|
|
658
994
|
const retiredKey = this.retiredUpdatesKey;
|
|
659
|
-
const merged =
|
|
995
|
+
const merged = Y.mergeUpdates(toSend.map((item) => item.update));
|
|
660
996
|
const controller = new AbortController();
|
|
661
|
-
if (keepalive)
|
|
662
|
-
this.keepaliveInFlight = true;
|
|
663
|
-
else {
|
|
997
|
+
if (!keepalive) {
|
|
664
998
|
this.updateInFlight = true;
|
|
665
999
|
this.updateAbortController = controller;
|
|
666
1000
|
}
|
|
667
1001
|
const timeout = setTimeout(() => controller.abort(), STATE_VECTOR_FETCH_TIMEOUT_MS);
|
|
1002
|
+
const userGeneration = this.userGeneration;
|
|
1003
|
+
const requireLiveCopyOutcome = toSend.some((item) => item.requireLiveCopyOutcome);
|
|
1004
|
+
const acknowledgedOwners = new Set();
|
|
1005
|
+
const ordinaryReceiptOwners = Array.from(this.subscribers.values()).filter((sub) => !sub.requireLiveCopyOutcome());
|
|
1006
|
+
let ack = null;
|
|
668
1007
|
try {
|
|
669
1008
|
const response = await fetch(`${this.baseUrl}/${this.docId}/update`, {
|
|
670
1009
|
method: "POST",
|
|
@@ -676,8 +1015,56 @@ class CollabDocConnection {
|
|
|
676
1015
|
signal: controller.signal,
|
|
677
1016
|
...(keepalive ? { keepalive: true } : {}),
|
|
678
1017
|
});
|
|
1018
|
+
// The same bytes are refused again, so a refused batch is sent in
|
|
1019
|
+
// halves, and a refused single update stops this tab's updates.
|
|
1020
|
+
if (this.keepsUnsentUpdates && response.status === 413) {
|
|
1021
|
+
if (userGeneration !== this.userGeneration)
|
|
1022
|
+
return;
|
|
1023
|
+
if (!toSend.some((item) => this.pendingUpdates.includes(item) &&
|
|
1024
|
+
!this.deliveredUpdates.has(item)))
|
|
1025
|
+
return;
|
|
1026
|
+
if (toSend.length > 1) {
|
|
1027
|
+
this.updateBatchLimit = Math.ceil(toSend.length / 2);
|
|
1028
|
+
}
|
|
1029
|
+
else {
|
|
1030
|
+
this.setSnapshot({ updatesRefused: true });
|
|
1031
|
+
}
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
679
1034
|
if (!response.ok)
|
|
680
1035
|
throw new Error(`HTTP ${response.status}`);
|
|
1036
|
+
if (userGeneration !== this.userGeneration)
|
|
1037
|
+
return;
|
|
1038
|
+
for (const item of toSend) {
|
|
1039
|
+
if (!this.deliveredUpdates.has(item)) {
|
|
1040
|
+
this.deliveredUpdates.add(item);
|
|
1041
|
+
if (item.requireLiveCopyOutcome)
|
|
1042
|
+
this.strictDeliveryVersion++;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
const parsedAck = requireLiveCopyOutcome ||
|
|
1046
|
+
Array.from(this.subscribers.values()).some((sub) => sub.onUpdateAcknowledged !== undefined && sub.canAcknowledge())
|
|
1047
|
+
? readUpdateAck(await response.json())
|
|
1048
|
+
: null;
|
|
1049
|
+
if (userGeneration !== this.userGeneration)
|
|
1050
|
+
return;
|
|
1051
|
+
if (requireLiveCopyOutcome) {
|
|
1052
|
+
if (!parsedAck?.liveCopy)
|
|
1053
|
+
throw new Error("The collab update response omitted its live-copy outcome.");
|
|
1054
|
+
for (const sub of this.subscribers.values()) {
|
|
1055
|
+
if (this.disposed || userGeneration !== this.userGeneration)
|
|
1056
|
+
break;
|
|
1057
|
+
if (sub.requireLiveCopyOutcome() &&
|
|
1058
|
+
sub.canAcknowledge() &&
|
|
1059
|
+
sub.onUpdateAcknowledged) {
|
|
1060
|
+
sub.onUpdateAcknowledged(parsedAck);
|
|
1061
|
+
acknowledgedOwners.add(sub);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
if (acknowledgedOwners.size === 0)
|
|
1065
|
+
throw new Error("The live-copy acknowledgment has no current strict subscriber.");
|
|
1066
|
+
}
|
|
1067
|
+
ack = parsedAck;
|
|
681
1068
|
const previous = this.pendingUpdates;
|
|
682
1069
|
const acknowledged = new Set(toSend);
|
|
683
1070
|
this.pendingUpdates = previous.filter((update) => !acknowledged.has(update));
|
|
@@ -693,31 +1080,60 @@ class CollabDocConnection {
|
|
|
693
1080
|
retiredCollabUpdates.delete(retiredKey);
|
|
694
1081
|
}
|
|
695
1082
|
}
|
|
1083
|
+
this.storeUnsentUpdates();
|
|
696
1084
|
this.updateErrors = 0;
|
|
1085
|
+
if (this.pendingUpdates.length === 0)
|
|
1086
|
+
this.updateBatchLimit = Infinity;
|
|
697
1087
|
}
|
|
698
1088
|
catch {
|
|
699
|
-
this.
|
|
1089
|
+
if (userGeneration === this.userGeneration)
|
|
1090
|
+
this.updateErrors++;
|
|
700
1091
|
}
|
|
701
1092
|
finally {
|
|
702
|
-
clearTimeout(timeout);
|
|
703
1093
|
if (keepalive)
|
|
704
|
-
this.
|
|
705
|
-
|
|
1094
|
+
this.keepaliveSnapshots.delete(toSend);
|
|
1095
|
+
clearTimeout(timeout);
|
|
1096
|
+
let online = false;
|
|
1097
|
+
if (!keepalive) {
|
|
706
1098
|
this.updateInFlight = false;
|
|
707
1099
|
this.updateAbortController = null;
|
|
1100
|
+
online = this.onlineDuringUpdate;
|
|
1101
|
+
this.onlineDuringUpdate = false;
|
|
708
1102
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
1103
|
+
this.schedulePendingFlush(online);
|
|
1104
|
+
}
|
|
1105
|
+
// Ordinary receipts keep nullable-ACK compatibility. Strict owners have
|
|
1106
|
+
// already handled their outcome before its pending bytes were removed.
|
|
1107
|
+
if (ack && !this.disposed && userGeneration === this.userGeneration) {
|
|
1108
|
+
const receiptOwners = [
|
|
1109
|
+
...ordinaryReceiptOwners.filter((sub) => !sub.requireLiveCopyOutcome()),
|
|
1110
|
+
...Array.from(this.subscribers.values()).filter((sub) => sub.requireLiveCopyOutcome()),
|
|
1111
|
+
];
|
|
1112
|
+
for (const sub of receiptOwners) {
|
|
1113
|
+
if (userGeneration === this.userGeneration &&
|
|
1114
|
+
!acknowledgedOwners.has(sub) &&
|
|
1115
|
+
sub.canAcknowledge() &&
|
|
1116
|
+
(!sub.requireLiveCopyOutcome() || ack.liveCopy !== null))
|
|
1117
|
+
sub.onUpdateAcknowledged?.(ack);
|
|
716
1118
|
}
|
|
717
1119
|
}
|
|
718
1120
|
}
|
|
1121
|
+
schedulePendingFlush(online) {
|
|
1122
|
+
if (this.disposed ||
|
|
1123
|
+
this.snapshot.updatesRefused ||
|
|
1124
|
+
this.pendingUpdates.length === 0 ||
|
|
1125
|
+
this.flushTimer)
|
|
1126
|
+
return;
|
|
1127
|
+
this.flushTimer = setTimeout(() => {
|
|
1128
|
+
this.flushTimer = null;
|
|
1129
|
+
void this.flushPendingUpdates();
|
|
1130
|
+
}, this.updateErrors && !online
|
|
1131
|
+
? calcBackoff(this.updateErrors)
|
|
1132
|
+
: UPDATE_DEBOUNCE_MS);
|
|
1133
|
+
}
|
|
719
1134
|
startSync() {
|
|
720
1135
|
if (this.syncActive ||
|
|
1136
|
+
this.signedOut ||
|
|
721
1137
|
this.docMissing ||
|
|
722
1138
|
this.disposed ||
|
|
723
1139
|
this.subscribers.size === 0) {
|
|
@@ -1008,7 +1424,9 @@ class CollabDocConnection {
|
|
|
1008
1424
|
if (!this.isPollCurrent(generation))
|
|
1009
1425
|
return;
|
|
1010
1426
|
const localState = this.awareness.getLocalState();
|
|
1011
|
-
if (localState &&
|
|
1427
|
+
if (localState &&
|
|
1428
|
+
this.canPublishPresence &&
|
|
1429
|
+
this.isPollCurrent(generation)) {
|
|
1012
1430
|
try {
|
|
1013
1431
|
const awarenessRes = await postAwarenessState(this.baseUrl, this.docId, this.ydoc.clientID, localState);
|
|
1014
1432
|
if (!this.isPollCurrent(generation))
|
|
@@ -1070,6 +1488,8 @@ class CollabDocConnection {
|
|
|
1070
1488
|
void this.poll(this.pollGeneration);
|
|
1071
1489
|
}
|
|
1072
1490
|
publishVisibility(visible) {
|
|
1491
|
+
if (!this.canPublishPresence || !this.awareness.getLocalState())
|
|
1492
|
+
return;
|
|
1073
1493
|
this.awareness.setLocalStateField("visible", visible);
|
|
1074
1494
|
const localState = this.awareness.getLocalState();
|
|
1075
1495
|
if (!localState)
|
|
@@ -1121,8 +1541,8 @@ class CollabDocConnection {
|
|
|
1121
1541
|
}
|
|
1122
1542
|
}
|
|
1123
1543
|
const collabConnectionRegistry = new Map();
|
|
1124
|
-
//
|
|
1125
|
-
//
|
|
1544
|
+
// In-memory retention survives component remounts; a connection with durable
|
|
1545
|
+
// updates also keeps them in localStorage. No retired retry loop.
|
|
1126
1546
|
const retiredCollabUpdates = new Map();
|
|
1127
1547
|
registerBeforeSignOutCleanup(async () => {
|
|
1128
1548
|
const results = await Promise.allSettled(Array.from(new Set(collabConnectionRegistry.values()), (connection) => connection.clearPresenceBeforeSignOut()));
|
|
@@ -1167,7 +1587,12 @@ export function _collabDocRegistrySizeForTests() {
|
|
|
1167
1587
|
return collabConnectionRegistry.size;
|
|
1168
1588
|
}
|
|
1169
1589
|
export function useCollaborativeDoc(options) {
|
|
1170
|
-
const { docId, pollInterval = 2000, pollIntervalWithSse = 12000, pauseWhenHidden = true, baseUrl = agentNativePath("/_agent-native/collab"), requestSource, user, activityResource, onEvent, } = options;
|
|
1590
|
+
const { docId, pollInterval = 2000, pollIntervalWithSse = 12000, pauseWhenHidden = true, baseUrl = agentNativePath("/_agent-native/collab"), requestSource, user, activityResource, durableUpdates = false, requireLiveCopyOutcome = false, onUpdateAcknowledged, onEvent, } = options;
|
|
1591
|
+
const onUpdateAcknowledgedRef = useRef(onUpdateAcknowledged);
|
|
1592
|
+
onUpdateAcknowledgedRef.current = onUpdateAcknowledged;
|
|
1593
|
+
const requireLiveCopyOutcomeRef = useRef(requireLiveCopyOutcome);
|
|
1594
|
+
requireLiveCopyOutcomeRef.current = requireLiveCopyOutcome;
|
|
1595
|
+
const wantsAcks = onUpdateAcknowledged !== undefined;
|
|
1171
1596
|
const onEventRef = useRef(onEvent);
|
|
1172
1597
|
onEventRef.current = onEvent;
|
|
1173
1598
|
useEffect(() => {
|
|
@@ -1199,6 +1624,9 @@ export function useCollaborativeDoc(options) {
|
|
|
1199
1624
|
return getOrCreateCollabConnection(docId, baseUrl, actorIdentity);
|
|
1200
1625
|
}, [docId, baseUrl, actorIdentity, generation, signedOutAfterAuthentication]);
|
|
1201
1626
|
const previousActorConnection = useRef({ actorIdentity, conn });
|
|
1627
|
+
const acknowledgementScope = useMemo(() => ({ conn, email: resolvedUser?.email }), [conn, resolvedUser?.email]);
|
|
1628
|
+
const activeAcknowledgementScope = useRef(acknowledgementScope);
|
|
1629
|
+
activeAcknowledgementScope.current = acknowledgementScope;
|
|
1202
1630
|
const [snapshot, setSnapshot] = useState(() => conn ? conn.snapshot : EMPTY_SNAPSHOT);
|
|
1203
1631
|
const [prevConn, setPrevConn] = useState(conn);
|
|
1204
1632
|
if (prevConn !== conn) {
|
|
@@ -1217,15 +1645,38 @@ export function useCollaborativeDoc(options) {
|
|
|
1217
1645
|
pollInterval,
|
|
1218
1646
|
pollIntervalWithSse,
|
|
1219
1647
|
pauseWhenHidden,
|
|
1220
|
-
requestSource,
|
|
1648
|
+
...(requestSource !== undefined ? { requestSource } : {}),
|
|
1649
|
+
durableUpdates,
|
|
1650
|
+
requireLiveCopyOutcome: () => activeAcknowledgementScope.current === acknowledgementScope &&
|
|
1651
|
+
requireLiveCopyOutcomeRef.current,
|
|
1652
|
+
canAcknowledge: () => activeAcknowledgementScope.current === acknowledgementScope &&
|
|
1653
|
+
onUpdateAcknowledgedRef.current !== undefined,
|
|
1654
|
+
...(wantsAcks
|
|
1655
|
+
? {
|
|
1656
|
+
onUpdateAcknowledged: (ack) => {
|
|
1657
|
+
if (activeAcknowledgementScope.current === acknowledgementScope)
|
|
1658
|
+
onUpdateAcknowledgedRef.current?.(ack);
|
|
1659
|
+
},
|
|
1660
|
+
}
|
|
1661
|
+
: {}),
|
|
1221
1662
|
onSnapshot: setSnapshot,
|
|
1222
1663
|
onEvent: (event) => onEventRef.current?.(event),
|
|
1223
|
-
});
|
|
1664
|
+
}, resolvedUser);
|
|
1224
1665
|
setSnapshot(conn.snapshot);
|
|
1225
1666
|
return () => {
|
|
1226
1667
|
conn.remove(id);
|
|
1227
1668
|
};
|
|
1228
|
-
}, [
|
|
1669
|
+
}, [
|
|
1670
|
+
conn,
|
|
1671
|
+
pollInterval,
|
|
1672
|
+
pollIntervalWithSse,
|
|
1673
|
+
pauseWhenHidden,
|
|
1674
|
+
requestSource,
|
|
1675
|
+
durableUpdates,
|
|
1676
|
+
requireLiveCopyOutcome,
|
|
1677
|
+
wantsAcks,
|
|
1678
|
+
wantsAcks ? acknowledgementScope : null,
|
|
1679
|
+
]);
|
|
1229
1680
|
useEffect(() => {
|
|
1230
1681
|
if (!conn || conn.detached || !resolvedUser)
|
|
1231
1682
|
return;
|
|
@@ -1259,6 +1710,8 @@ export function useCollaborativeDoc(options) {
|
|
|
1259
1710
|
: () => { },
|
|
1260
1711
|
requestSync: conn ? conn.requestSync : requestSyncUnavailable,
|
|
1261
1712
|
flushUpdates: conn ? conn.flushUpdates : flushUpdatesUnavailable,
|
|
1713
|
+
durableUpdatesFailed: snapshot.durableUpdatesFailed,
|
|
1714
|
+
updatesRefused: snapshot.updatesRefused,
|
|
1262
1715
|
activeUsers: snapshot.activeUsers,
|
|
1263
1716
|
agentActive: snapshot.agentActive,
|
|
1264
1717
|
agentPresent: snapshot.agentPresent,
|