@yorkie-js/sdk 0.7.7 → 0.7.8

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.
@@ -362,7 +362,7 @@
362
362
  }
363
363
  }
364
364
  const IMPLICIT$3 = 2;
365
- const unsafeLocal = Symbol.for("reflect unsafe local");
365
+ const unsafeLocal = /* @__PURE__ */ Symbol.for("reflect unsafe local");
366
366
  function unsafeOneofCase(target, oneof) {
367
367
  const c = target[oneof.localName].case;
368
368
  if (c === void 0) {
@@ -588,7 +588,7 @@
588
588
  }
589
589
  return ret;
590
590
  }
591
- const tokenZeroMessageField = Symbol();
591
+ const tokenZeroMessageField = /* @__PURE__ */ Symbol();
592
592
  const messagePrototypes = /* @__PURE__ */ new WeakMap();
593
593
  function createZeroMessage(desc) {
594
594
  let msg;
@@ -690,7 +690,7 @@
690
690
  function isFieldError(arg) {
691
691
  return arg instanceof Error && errorNames.includes(arg.name) && "field" in arg && typeof arg.field == "function";
692
692
  }
693
- const symbol = Symbol.for("@bufbuild/protobuf/text-encoding");
693
+ const symbol = /* @__PURE__ */ Symbol.for("@bufbuild/protobuf/text-encoding");
694
694
  function getTextEncoding() {
695
695
  if (globalThis[symbol] == void 0) {
696
696
  const te = new globalThis.TextEncoder();
@@ -4058,7 +4058,7 @@
4058
4058
  msg.set(field, scalarValue);
4059
4059
  }
4060
4060
  }
4061
- const tokenIgnoredUnknownEnum = Symbol();
4061
+ const tokenIgnoredUnknownEnum = /* @__PURE__ */ Symbol();
4062
4062
  function readEnum(desc, json, ignoreUnknownFields, nullAsZeroValue) {
4063
4063
  if (json === null) {
4064
4064
  if (desc.typeName == "google.protobuf.NullValue") {
@@ -4084,7 +4084,7 @@
4084
4084
  }
4085
4085
  throw new Error(`cannot decode ${desc} from JSON: ${formatVal(json)}`);
4086
4086
  }
4087
- const tokenNull = Symbol();
4087
+ const tokenNull = /* @__PURE__ */ Symbol();
4088
4088
  function scalarFromJson(field, json, nullAsZeroValue) {
4089
4089
  if (json === null) {
4090
4090
  if (nullAsZeroValue) {
@@ -5905,6 +5905,8 @@
5905
5905
  this.message = message;
5906
5906
  this.toString = () => `[code=${this.code}]: ${this.message}`;
5907
5907
  }
5908
+ code;
5909
+ message;
5908
5910
  name = "YorkieError";
5909
5911
  stack;
5910
5912
  }
@@ -12534,8 +12536,10 @@
12534
12536
  break;
12535
12537
  }
12536
12538
  if (next.parent && next.parent === toParent) {
12537
- collectFromLeft = next;
12538
- collectFromParent = toParent;
12539
+ if (toLeft !== toParent) {
12540
+ collectFromLeft = next;
12541
+ collectFromParent = toParent;
12542
+ }
12539
12543
  break;
12540
12544
  }
12541
12545
  current = next;
@@ -12598,6 +12602,7 @@
12598
12602
  tokensToBeRemoved,
12599
12603
  editedAt
12600
12604
  );
12605
+ const mergeLevel = toBeMergedNodes.length;
12601
12606
  const pairs = [];
12602
12607
  for (const node of nodesToBeRemoved) {
12603
12608
  if (node.remove(editedAt)) {
@@ -12724,7 +12729,15 @@
12724
12729
  }
12725
12730
  }
12726
12731
  }
12727
- return [changes, pairs, diff, nodesToBeRemoved, fromIdx];
12732
+ return [
12733
+ changes,
12734
+ pairs,
12735
+ diff,
12736
+ nodesToBeRemoved,
12737
+ fromIdx,
12738
+ mergeLevel,
12739
+ preTombstoned
12740
+ ];
12728
12741
  }
12729
12742
  /**
12730
12743
  * `editT` edits the given range with the given value.
@@ -13165,11 +13178,35 @@
13165
13178
  return [prev, prev.isText ? TokenType.Text : TokenType.End];
13166
13179
  }
13167
13180
  }
13168
- function clearRemovedAt(node) {
13169
- traverseAll(node, (n) => {
13181
+ function cloneAndDropPreTombstoned(node, preTombstoned) {
13182
+ const clone = node.deepcopy();
13183
+ filterChildren(clone, preTombstoned);
13184
+ traverseAll(clone, (n) => {
13170
13185
  n.removedAt = void 0;
13171
- n.visibleSize = n.totalSize;
13186
+ if (n.isText) {
13187
+ n.visibleSize = n.value.length;
13188
+ n.totalSize = n.value.length;
13189
+ return;
13190
+ }
13191
+ let size = 0;
13192
+ for (const child of n._children) size += child.paddedSize();
13193
+ n.visibleSize = size;
13194
+ n.totalSize = size;
13172
13195
  });
13196
+ return clone;
13197
+ }
13198
+ function filterChildren(node, preTombstoned) {
13199
+ const all = node._children;
13200
+ if (!all) return;
13201
+ const kept = [];
13202
+ for (const child of all) {
13203
+ if (preTombstoned.has(child.id.toIDString())) {
13204
+ continue;
13205
+ }
13206
+ filterChildren(child, preTombstoned);
13207
+ kept.push(child);
13208
+ }
13209
+ node._children = kept;
13173
13210
  }
13174
13211
  class TreeEditOperation extends Operation {
13175
13212
  fromPos;
@@ -13241,7 +13278,15 @@
13241
13278
  this.toPos = tree.findPos(this.toIdx);
13242
13279
  }
13243
13280
  }
13244
- const [changes, pairs, diff, removedNodes, preEditFromIdx] = tree.edit(
13281
+ const [
13282
+ changes,
13283
+ pairs,
13284
+ diff,
13285
+ removedNodes,
13286
+ preEditFromIdx,
13287
+ mergeLevel,
13288
+ preTombstoned
13289
+ ] = tree.edit(
13245
13290
  [this.fromPos, this.toPos],
13246
13291
  this.contents?.map((content) => content.deepcopy()),
13247
13292
  this.splitLevel,
@@ -13276,7 +13321,13 @@
13276
13321
  let reverseOp;
13277
13322
  const isPureSplit = this.splitLevel > 0 && !this.contents?.length && removedNodes.length === 0;
13278
13323
  if (this.splitLevel === 0) {
13279
- reverseOp = this.toReverseOperation(tree, removedNodes, preEditFromIdx);
13324
+ reverseOp = this.toReverseOperation(
13325
+ tree,
13326
+ removedNodes,
13327
+ preEditFromIdx,
13328
+ preTombstoned,
13329
+ mergeLevel
13330
+ );
13280
13331
  } else if (isPureSplit) {
13281
13332
  reverseOp = this.toSplitReverseOperation(tree, preEditFromIdx);
13282
13333
  }
@@ -13314,7 +13365,7 @@
13314
13365
  * @param removedNodes - Nodes that were removed by this edit
13315
13366
  * @param preEditFromIdx - The from index captured BEFORE the edit
13316
13367
  */
13317
- toReverseOperation(tree, removedNodes, preEditFromIdx) {
13368
+ toReverseOperation(tree, removedNodes, preEditFromIdx, preTombstoned, mergeLevel) {
13318
13369
  if (this.redoSplitLevel !== void 0 && this.redoSplitLevel > 0) {
13319
13370
  const splitRedoFromPos = tree.findPos(preEditFromIdx);
13320
13371
  const splitRedoOp = TreeEditOperation.create(
@@ -13333,19 +13384,36 @@
13333
13384
  );
13334
13385
  return splitRedoOp;
13335
13386
  }
13387
+ if (mergeLevel && mergeLevel > 0) {
13388
+ const splitFromPos = tree.findPos(preEditFromIdx);
13389
+ const splitUndoOp = TreeEditOperation.create(
13390
+ this.getParentCreatedAt(),
13391
+ splitFromPos,
13392
+ splitFromPos,
13393
+ void 0,
13394
+ // no inserted content — split creates boundaries
13395
+ mergeLevel,
13396
+ // splitLevel = number of merged boundaries
13397
+ void 0,
13398
+ // executedAt assigned at undo time
13399
+ true,
13400
+ // isUndoOp
13401
+ preEditFromIdx,
13402
+ preEditFromIdx
13403
+ );
13404
+ return splitUndoOp;
13405
+ }
13336
13406
  const insertedContentSize = this.contents ? this.contents.reduce((sum, node) => sum + node.paddedSize(), 0) : 0;
13337
13407
  const maxNeededIdx = preEditFromIdx + insertedContentSize;
13338
13408
  if (maxNeededIdx > tree.getSize()) {
13339
13409
  return void 0;
13340
13410
  }
13341
13411
  const topLevelRemoved = removedNodes.filter(
13342
- (node) => !node.parent || !removedNodes.includes(node.parent)
13412
+ (node) => !preTombstoned.has(node.id.toIDString()) && (!node.parent || !removedNodes.includes(node.parent))
13343
13413
  );
13344
- const reverseContents = topLevelRemoved.length > 0 ? topLevelRemoved.map((n) => {
13345
- const clone = n.deepcopy();
13346
- clearRemovedAt(clone);
13347
- return clone;
13348
- }) : void 0;
13414
+ const reverseContents = topLevelRemoved.length > 0 ? topLevelRemoved.map(
13415
+ (n) => cloneAndDropPreTombstoned(n, preTombstoned)
13416
+ ) : void 0;
13349
13417
  const reverseFromPos = tree.findPos(preEditFromIdx);
13350
13418
  let reverseToPos;
13351
13419
  if (insertedContentSize > 0) {
@@ -20164,6 +20232,11 @@
20164
20232
  }
20165
20233
  ]);
20166
20234
  }
20235
+ /**
20236
+ * `clearHistory` flushes both undo and redo stacks. This is used
20237
+ * after applying a snapshot or initialRoot so that setup operations
20238
+ * are not reachable via undo.
20239
+ */
20167
20240
  clearHistory() {
20168
20241
  this.internalHistory.clearRedo();
20169
20242
  this.internalHistory.clearUndo();
@@ -20623,10 +20696,7 @@
20623
20696
  }
20624
20697
  const ops = isUndo ? this.internalHistory.popUndo() : this.internalHistory.popRedo();
20625
20698
  if (!ops) {
20626
- throw new YorkieError(
20627
- Code.ErrRefused,
20628
- `There is no operation to be ${isUndo ? "undone" : "redone"}`
20629
- );
20699
+ return;
20630
20700
  }
20631
20701
  this.ensureClone();
20632
20702
  const ctx = ChangeContext.create(
@@ -20719,6 +20789,8 @@
20719
20789
  syncMode;
20720
20790
  changeEventReceived;
20721
20791
  lastHeartbeatTime;
20792
+ pollInterval;
20793
+ pollIntervalPinned;
20722
20794
  reconnectStreamDelay;
20723
20795
  cancelled;
20724
20796
  watchStream;
@@ -20726,13 +20798,15 @@
20726
20798
  watchAbortController;
20727
20799
  syncPromise;
20728
20800
  _detaching = false;
20729
- constructor(reconnectStreamDelay, resource, resourceID, syncMode) {
20801
+ constructor(reconnectStreamDelay, resource, resourceID, syncMode, pollInterval = 0, pollIntervalPinned = false) {
20730
20802
  this.reconnectStreamDelay = reconnectStreamDelay;
20731
20803
  this.resource = resource;
20732
20804
  this.resourceID = resourceID;
20733
20805
  this.syncMode = syncMode;
20734
20806
  this.changeEventReceived = syncMode !== void 0 ? false : void 0;
20735
20807
  this.lastHeartbeatTime = Date.now();
20808
+ this.pollInterval = pollInterval;
20809
+ this.pollIntervalPinned = pollIntervalPinned;
20736
20810
  this.cancelled = false;
20737
20811
  }
20738
20812
  /**
@@ -20752,6 +20826,9 @@
20752
20826
  if (this.syncMode === SyncMode.RealtimePushOnly) {
20753
20827
  return this.resource.hasLocalChanges();
20754
20828
  }
20829
+ if (this.syncMode === SyncMode.Polling) {
20830
+ return Date.now() - this.lastHeartbeatTime >= this.pollInterval;
20831
+ }
20755
20832
  return this.syncMode !== SyncMode.Manual && (this.resource.hasLocalChanges() || (this.changeEventReceived ?? false));
20756
20833
  }
20757
20834
  /**
@@ -20765,7 +20842,8 @@
20765
20842
  if (this.syncMode === SyncMode.Manual) {
20766
20843
  return false;
20767
20844
  }
20768
- return Date.now() - this.lastHeartbeatTime >= heartbeatInterval;
20845
+ const interval = this.pollInterval > 0 ? this.pollInterval : heartbeatInterval;
20846
+ return Date.now() - this.lastHeartbeatTime >= interval;
20769
20847
  }
20770
20848
  /**
20771
20849
  * `updateHeartbeatTime` updates the last heartbeat time.
@@ -20844,6 +20922,16 @@
20844
20922
  }
20845
20923
  }
20846
20924
  }
20925
+ /**
20926
+ * `resetCancelled` clears the cancelled flag so the watch loop can run again
20927
+ * after a previous cancellation (e.g., after changeSyncMode back to Realtime).
20928
+ * Caller must invoke `runWatchLoop` immediately after to claim the stream slot;
20929
+ * `doLoop`'s `if (this.watchStream)` guard prevents double-stream creation if a
20930
+ * delayed `onDisconnect` callback from the previously-cancelled stream races.
20931
+ */
20932
+ resetCancelled() {
20933
+ this.cancelled = false;
20934
+ }
20847
20935
  /**
20848
20936
  * `cancelWatchStream` cancels the watch stream.
20849
20937
  */
@@ -20878,7 +20966,7 @@
20878
20966
  };
20879
20967
  }
20880
20968
  const name = "@yorkie-js/sdk";
20881
- const version = "0.7.7";
20969
+ const version = "0.7.8";
20882
20970
  const pkg = {
20883
20971
  name,
20884
20972
  version
@@ -21161,6 +21249,7 @@
21161
21249
  SyncMode2["Realtime"] = "realtime";
21162
21250
  SyncMode2["RealtimePushOnly"] = "realtime-pushonly";
21163
21251
  SyncMode2["RealtimeSyncOff"] = "realtime-syncoff";
21252
+ SyncMode2["Polling"] = "polling";
21164
21253
  return SyncMode2;
21165
21254
  })(SyncMode || {});
21166
21255
  var ClientStatus = /* @__PURE__ */ ((ClientStatus2) => {
@@ -21173,6 +21262,7 @@
21173
21262
  ClientCondition2["WatchLoop"] = "WatchLoop";
21174
21263
  return ClientCondition2;
21175
21264
  })(ClientCondition || {});
21265
+ const DefaultPollingIntervalMs = 3e3;
21176
21266
  const DefaultClientOptions = {
21177
21267
  rpcAddr: "https://api.yorkie.dev",
21178
21268
  syncLoopDuration: 50,
@@ -21368,6 +21458,14 @@
21368
21458
  doc.setActor(this.id);
21369
21459
  doc.update((_, p) => p.set(opts.initialPresence || {}));
21370
21460
  const syncMode = opts.syncMode ?? "realtime";
21461
+ if (opts.documentPollInterval !== void 0 && opts.documentPollInterval <= 0) {
21462
+ throw new YorkieError(
21463
+ Code.ErrInvalidArgument,
21464
+ "documentPollInterval must be greater than 0"
21465
+ );
21466
+ }
21467
+ const pollIntervalPinned = opts.documentPollInterval !== void 0;
21468
+ const pollInterval = pollIntervalPinned ? opts.documentPollInterval : syncMode === "polling" ? DefaultPollingIntervalMs : 0;
21371
21469
  return this.enqueueTask(async () => {
21372
21470
  try {
21373
21471
  const res = await this.rpcClient.attachDocument(
@@ -21397,10 +21495,12 @@
21397
21495
  this.reconnectStreamDelay,
21398
21496
  doc,
21399
21497
  res.documentId,
21400
- syncMode
21498
+ syncMode,
21499
+ pollInterval,
21500
+ pollIntervalPinned
21401
21501
  )
21402
21502
  );
21403
- if (syncMode !== "manual") {
21503
+ if (syncMode !== "manual" && syncMode !== "polling") {
21404
21504
  await this.runWatchLoop(doc.getKey());
21405
21505
  }
21406
21506
  logger.info(`[AD] c:"${this.getKey()}" attaches d:"${doc.getKey()}"`);
@@ -21416,6 +21516,7 @@
21416
21516
  }
21417
21517
  });
21418
21518
  }
21519
+ doc.clearHistory();
21419
21520
  return doc;
21420
21521
  } catch (err) {
21421
21522
  logger.error(`[AD] c:"${this.getKey()}" err :`, err);
@@ -21526,12 +21627,23 @@
21526
21627
  channel.setSessionID(res.sessionId);
21527
21628
  channel.updateSessionCount(Number(res.sessionCount), 0);
21528
21629
  channel.applyStatus(ChannelStatus.Attached);
21529
- const syncMode = opts.isRealtime !== false ? "realtime" : "manual";
21630
+ const syncMode = opts.syncMode ?? "realtime";
21631
+ this.assertValidChannelSyncMode(syncMode);
21632
+ if (opts.channelHeartbeatInterval !== void 0 && opts.channelHeartbeatInterval <= 0) {
21633
+ throw new YorkieError(
21634
+ Code.ErrInvalidArgument,
21635
+ "channelHeartbeatInterval must be greater than 0"
21636
+ );
21637
+ }
21638
+ const pollIntervalPinned = opts.channelHeartbeatInterval !== void 0;
21639
+ const pollInterval = pollIntervalPinned ? opts.channelHeartbeatInterval : syncMode === "polling" ? DefaultPollingIntervalMs : this.channelHeartbeatInterval;
21530
21640
  const attachment = new Attachment(
21531
21641
  this.reconnectStreamDelay,
21532
21642
  channel,
21533
21643
  res.sessionId,
21534
- syncMode
21644
+ syncMode,
21645
+ pollInterval,
21646
+ pollIntervalPinned
21535
21647
  );
21536
21648
  channel.subscribe("local-broadcast", (event) => {
21537
21649
  const { topic, payload, options } = event;
@@ -21607,9 +21719,17 @@
21607
21719
  return this.enqueueTask(task);
21608
21720
  }
21609
21721
  /**
21610
- * `changeSyncMode` changes the synchronization mode of the given document.
21722
+ * `changeSyncMode` changes the synchronization mode of the given resource.
21611
21723
  */
21612
- async changeSyncMode(doc, syncMode) {
21724
+ async changeSyncMode(resource, syncMode) {
21725
+ return this.enqueueTask(async () => {
21726
+ if (resource instanceof Channel) {
21727
+ return this.changeChannelSyncMode(resource, syncMode);
21728
+ }
21729
+ return this.changeDocumentSyncMode(resource, syncMode);
21730
+ });
21731
+ }
21732
+ async changeDocumentSyncMode(doc, syncMode) {
21613
21733
  if (!this.isActive()) {
21614
21734
  throw new YorkieError(
21615
21735
  Code.ErrClientNotActivated,
@@ -21627,19 +21747,66 @@
21627
21747
  if (prevSyncMode === syncMode) {
21628
21748
  return doc;
21629
21749
  }
21630
- attachment.changeSyncMode(syncMode);
21631
- if (syncMode === "manual") {
21750
+ if (syncMode === "manual" || syncMode === "polling") {
21632
21751
  attachment.cancelWatchStream();
21633
- return doc;
21634
21752
  }
21753
+ attachment.changeSyncMode(syncMode);
21635
21754
  if (syncMode === "realtime") {
21636
21755
  attachment.changeEventReceived = true;
21637
21756
  }
21638
- if (prevSyncMode === "manual") {
21757
+ if (!attachment.pollIntervalPinned) {
21758
+ attachment.pollInterval = syncMode === "polling" ? DefaultPollingIntervalMs : 0;
21759
+ }
21760
+ if ((prevSyncMode === "manual" || prevSyncMode === "polling") && syncMode !== "manual" && syncMode !== "polling") {
21761
+ attachment.resetCancelled();
21639
21762
  await this.runWatchLoop(doc.getKey());
21640
21763
  }
21641
21764
  return doc;
21642
21765
  }
21766
+ /**
21767
+ * `assertValidChannelSyncMode` rejects sync modes that are not valid for
21768
+ * channels. `RealtimePushOnly` and `RealtimeSyncOff` are document-only.
21769
+ */
21770
+ assertValidChannelSyncMode(syncMode) {
21771
+ if (syncMode !== "manual" && syncMode !== "realtime" && syncMode !== "polling") {
21772
+ throw new YorkieError(
21773
+ Code.ErrInvalidArgument,
21774
+ `invalid channel sync mode: ${syncMode}`
21775
+ );
21776
+ }
21777
+ }
21778
+ async changeChannelSyncMode(channel, syncMode) {
21779
+ if (!this.isActive()) {
21780
+ throw new YorkieError(
21781
+ Code.ErrClientNotActivated,
21782
+ `${this.key} is not active`
21783
+ );
21784
+ }
21785
+ const attachment = this.attachmentMap.get(channel.getKey());
21786
+ if (!attachment) {
21787
+ throw new YorkieError(
21788
+ Code.ErrNotAttached,
21789
+ `${channel.getKey()} is not attached`
21790
+ );
21791
+ }
21792
+ const prevSyncMode = attachment.syncMode;
21793
+ if (prevSyncMode === syncMode) {
21794
+ return channel;
21795
+ }
21796
+ this.assertValidChannelSyncMode(syncMode);
21797
+ if (prevSyncMode === "realtime") {
21798
+ attachment.cancelWatchStream();
21799
+ }
21800
+ attachment.changeSyncMode(syncMode);
21801
+ if (!attachment.pollIntervalPinned) {
21802
+ attachment.pollInterval = syncMode === "polling" ? DefaultPollingIntervalMs : syncMode === "realtime" ? this.channelHeartbeatInterval : 0;
21803
+ }
21804
+ if (syncMode === "realtime") {
21805
+ attachment.resetCancelled();
21806
+ await this.runWatchLoop(channel.getKey());
21807
+ }
21808
+ return channel;
21809
+ }
21643
21810
  /**
21644
21811
  * `sync` implementation that handles both Document and Channel.
21645
21812
  */
@@ -22484,6 +22651,7 @@
22484
22651
  return doc;
22485
22652
  }
22486
22653
  doc.applyChangePack(respPack);
22654
+ attachment.updateHeartbeatTime();
22487
22655
  attachment.resource.publish([
22488
22656
  {
22489
22657
  type: DocEventType.SyncStatusChanged,