@yorkie-js/sdk 0.7.6 → 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
  }
@@ -11134,6 +11136,32 @@
11134
11136
  }
11135
11137
  actualRight.push(child);
11136
11138
  }
11139
+ if (versionVector) {
11140
+ const movedToLeft = [];
11141
+ const remaining = [];
11142
+ let boundaryReached = false;
11143
+ for (const child of actualRight) {
11144
+ if (!boundaryReached) {
11145
+ if (child.insPrevID !== void 0 && !child.isText) {
11146
+ remaining.push(child);
11147
+ continue;
11148
+ }
11149
+ const actorID = child.id.getCreatedAt().getActorID();
11150
+ const knownLamport = versionVector.get(actorID);
11151
+ if (knownLamport === void 0 || knownLamport < child.id.getCreatedAt().getLamport()) {
11152
+ movedToLeft.push(child);
11153
+ continue;
11154
+ }
11155
+ }
11156
+ boundaryReached = true;
11157
+ remaining.push(child);
11158
+ }
11159
+ if (movedToLeft.length > 0) {
11160
+ left.push(...movedToLeft);
11161
+ actualRight.length = 0;
11162
+ actualRight.push(...remaining);
11163
+ }
11164
+ }
11137
11165
  this._children = left;
11138
11166
  clone._children = actualRight;
11139
11167
  this.visibleSize = this._children.reduce(
@@ -11910,8 +11938,14 @@
11910
11938
  split.insPrevID = this.id;
11911
11939
  if (this.insNextID) {
11912
11940
  const insNext = tree.findFloorNode(this.insNextID);
11913
- insNext.insPrevID = split.id;
11914
11941
  split.insNextID = this.insNextID;
11942
+ if (insNext) {
11943
+ insNext.insPrevID = split.id;
11944
+ if (!this.isText && insNext.parent && !insNext.isRemoved && insNext.parent !== split.parent && split.allChildren.length === 0) {
11945
+ split.parent.detachChild(split);
11946
+ insNext.parent.insertBefore(split, insNext);
11947
+ }
11948
+ }
11915
11949
  }
11916
11950
  this.insNextID = split.id;
11917
11951
  tree.registerNode(split);
@@ -12135,7 +12169,7 @@
12135
12169
  * given node, advancing past element-type split siblings that the editing
12136
12170
  * client did not know about (not in versionVector).
12137
12171
  */
12138
- advancePastUnknownSplitSiblings(node, versionVector) {
12172
+ advancePastUnknownSplitSiblings(node, versionVector, relaxParentCheck = false, skipActorID) {
12139
12173
  if (!versionVector || !node) {
12140
12174
  return node;
12141
12175
  }
@@ -12145,10 +12179,13 @@
12145
12179
  if (!next || next.isText) {
12146
12180
  break;
12147
12181
  }
12148
- if (next.parent !== current.parent) {
12182
+ if (!relaxParentCheck && next.parent !== current.parent) {
12149
12183
  break;
12150
12184
  }
12151
12185
  const actorID = next.id.getCreatedAt().getActorID();
12186
+ if (skipActorID !== void 0 && actorID === skipActorID) {
12187
+ break;
12188
+ }
12152
12189
  const knownLamport = versionVector.get(actorID);
12153
12190
  if (knownLamport !== void 0 && knownLamport >= next.id.getCreatedAt().getLamport()) {
12154
12191
  break;
@@ -12489,6 +12526,25 @@
12489
12526
  addDataSizes(diff, diffTo, diffFrom);
12490
12527
  const fromLeft = fromLeftRaw !== fromParent ? this.advancePastUnknownSplitSiblings(fromLeftRaw, versionVector) : fromLeftRaw;
12491
12528
  const toLeft = toLeftRaw !== toParent ? this.advancePastUnknownSplitSiblings(toLeftRaw, versionVector) : toLeftRaw;
12529
+ let collectFromParent = fromParent;
12530
+ let collectFromLeft = fromLeft;
12531
+ if (fromLeft !== fromParent && fromParent !== toParent) {
12532
+ let current = fromLeft;
12533
+ while (current.insNextID) {
12534
+ const next = this.findFloorNode(current.insNextID);
12535
+ if (!next || next.isText) {
12536
+ break;
12537
+ }
12538
+ if (next.parent && next.parent === toParent) {
12539
+ if (toLeft !== toParent) {
12540
+ collectFromLeft = next;
12541
+ collectFromParent = toParent;
12542
+ }
12543
+ break;
12544
+ }
12545
+ current = next;
12546
+ }
12547
+ }
12492
12548
  const fromIdx = this.toIndex(fromParent, fromLeft);
12493
12549
  const fromPath = this.toPath(fromParent, fromLeft);
12494
12550
  const nodesToBeRemoved = [];
@@ -12497,8 +12553,8 @@
12497
12553
  const toBeMergedNodes = [];
12498
12554
  const preTombstoned = /* @__PURE__ */ new Set();
12499
12555
  this.traverseInPosRange(
12500
- fromParent,
12501
- fromLeft,
12556
+ collectFromParent,
12557
+ collectFromLeft,
12502
12558
  toParent,
12503
12559
  toLeft,
12504
12560
  ([node, tokenType], ended) => {
@@ -12546,6 +12602,7 @@
12546
12602
  tokensToBeRemoved,
12547
12603
  editedAt
12548
12604
  );
12605
+ const mergeLevel = toBeMergedNodes.length;
12549
12606
  const pairs = [];
12550
12607
  for (const node of nodesToBeRemoved) {
12551
12608
  if (node.remove(editedAt)) {
@@ -12602,9 +12659,20 @@
12602
12659
  let parent = fromParent;
12603
12660
  let left = fromLeft;
12604
12661
  while (splitCount < splitLevel) {
12662
+ if (left !== parent) {
12663
+ left = this.advancePastUnknownSplitSiblings(
12664
+ left,
12665
+ versionVector,
12666
+ true,
12667
+ editedAt.getActorID()
12668
+ );
12669
+ if (left.parent && left.parent !== parent) {
12670
+ parent = left.parent;
12671
+ }
12672
+ }
12605
12673
  parent.split(
12606
12674
  this,
12607
- parent.findOffset(left, true) + 1,
12675
+ left !== parent ? parent.findOffset(left, true) + 1 : 0,
12608
12676
  issueTimeTicket,
12609
12677
  versionVector
12610
12678
  );
@@ -12661,7 +12729,15 @@
12661
12729
  }
12662
12730
  }
12663
12731
  }
12664
- return [changes, pairs, diff, nodesToBeRemoved, fromIdx];
12732
+ return [
12733
+ changes,
12734
+ pairs,
12735
+ diff,
12736
+ nodesToBeRemoved,
12737
+ fromIdx,
12738
+ mergeLevel,
12739
+ preTombstoned
12740
+ ];
12665
12741
  }
12666
12742
  /**
12667
12743
  * `editT` edits the given range with the given value.
@@ -13102,11 +13178,35 @@
13102
13178
  return [prev, prev.isText ? TokenType.Text : TokenType.End];
13103
13179
  }
13104
13180
  }
13105
- function clearRemovedAt(node) {
13106
- traverseAll(node, (n) => {
13181
+ function cloneAndDropPreTombstoned(node, preTombstoned) {
13182
+ const clone = node.deepcopy();
13183
+ filterChildren(clone, preTombstoned);
13184
+ traverseAll(clone, (n) => {
13107
13185
  n.removedAt = void 0;
13108
- 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;
13109
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;
13110
13210
  }
13111
13211
  class TreeEditOperation extends Operation {
13112
13212
  fromPos;
@@ -13178,7 +13278,15 @@
13178
13278
  this.toPos = tree.findPos(this.toIdx);
13179
13279
  }
13180
13280
  }
13181
- 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(
13182
13290
  [this.fromPos, this.toPos],
13183
13291
  this.contents?.map((content) => content.deepcopy()),
13184
13292
  this.splitLevel,
@@ -13211,10 +13319,16 @@
13211
13319
  );
13212
13320
  this.lastToIdx = preEditFromIdx + removedSize;
13213
13321
  let reverseOp;
13214
- const isPureL1Split = this.splitLevel === 1 && !this.contents?.length && removedNodes.length === 0;
13322
+ const isPureSplit = this.splitLevel > 0 && !this.contents?.length && removedNodes.length === 0;
13215
13323
  if (this.splitLevel === 0) {
13216
- reverseOp = this.toReverseOperation(tree, removedNodes, preEditFromIdx);
13217
- } else if (isPureL1Split) {
13324
+ reverseOp = this.toReverseOperation(
13325
+ tree,
13326
+ removedNodes,
13327
+ preEditFromIdx,
13328
+ preTombstoned,
13329
+ mergeLevel
13330
+ );
13331
+ } else if (isPureSplit) {
13218
13332
  reverseOp = this.toSplitReverseOperation(tree, preEditFromIdx);
13219
13333
  }
13220
13334
  root.acc(diff);
@@ -13251,7 +13365,7 @@
13251
13365
  * @param removedNodes - Nodes that were removed by this edit
13252
13366
  * @param preEditFromIdx - The from index captured BEFORE the edit
13253
13367
  */
13254
- toReverseOperation(tree, removedNodes, preEditFromIdx) {
13368
+ toReverseOperation(tree, removedNodes, preEditFromIdx, preTombstoned, mergeLevel) {
13255
13369
  if (this.redoSplitLevel !== void 0 && this.redoSplitLevel > 0) {
13256
13370
  const splitRedoFromPos = tree.findPos(preEditFromIdx);
13257
13371
  const splitRedoOp = TreeEditOperation.create(
@@ -13270,19 +13384,36 @@
13270
13384
  );
13271
13385
  return splitRedoOp;
13272
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
+ }
13273
13406
  const insertedContentSize = this.contents ? this.contents.reduce((sum, node) => sum + node.paddedSize(), 0) : 0;
13274
13407
  const maxNeededIdx = preEditFromIdx + insertedContentSize;
13275
13408
  if (maxNeededIdx > tree.getSize()) {
13276
13409
  return void 0;
13277
13410
  }
13278
13411
  const topLevelRemoved = removedNodes.filter(
13279
- (node) => !node.parent || !removedNodes.includes(node.parent)
13412
+ (node) => !preTombstoned.has(node.id.toIDString()) && (!node.parent || !removedNodes.includes(node.parent))
13280
13413
  );
13281
- const reverseContents = topLevelRemoved.length > 0 ? topLevelRemoved.map((n) => {
13282
- const clone = n.deepcopy();
13283
- clearRemovedAt(clone);
13284
- return clone;
13285
- }) : void 0;
13414
+ const reverseContents = topLevelRemoved.length > 0 ? topLevelRemoved.map(
13415
+ (n) => cloneAndDropPreTombstoned(n, preTombstoned)
13416
+ ) : void 0;
13286
13417
  const reverseFromPos = tree.findPos(preEditFromIdx);
13287
13418
  let reverseToPos;
13288
13419
  if (insertedContentSize > 0) {
@@ -20101,6 +20232,11 @@
20101
20232
  }
20102
20233
  ]);
20103
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
+ */
20104
20240
  clearHistory() {
20105
20241
  this.internalHistory.clearRedo();
20106
20242
  this.internalHistory.clearUndo();
@@ -20560,10 +20696,7 @@
20560
20696
  }
20561
20697
  const ops = isUndo ? this.internalHistory.popUndo() : this.internalHistory.popRedo();
20562
20698
  if (!ops) {
20563
- throw new YorkieError(
20564
- Code.ErrRefused,
20565
- `There is no operation to be ${isUndo ? "undone" : "redone"}`
20566
- );
20699
+ return;
20567
20700
  }
20568
20701
  this.ensureClone();
20569
20702
  const ctx = ChangeContext.create(
@@ -20656,6 +20789,8 @@
20656
20789
  syncMode;
20657
20790
  changeEventReceived;
20658
20791
  lastHeartbeatTime;
20792
+ pollInterval;
20793
+ pollIntervalPinned;
20659
20794
  reconnectStreamDelay;
20660
20795
  cancelled;
20661
20796
  watchStream;
@@ -20663,13 +20798,15 @@
20663
20798
  watchAbortController;
20664
20799
  syncPromise;
20665
20800
  _detaching = false;
20666
- constructor(reconnectStreamDelay, resource, resourceID, syncMode) {
20801
+ constructor(reconnectStreamDelay, resource, resourceID, syncMode, pollInterval = 0, pollIntervalPinned = false) {
20667
20802
  this.reconnectStreamDelay = reconnectStreamDelay;
20668
20803
  this.resource = resource;
20669
20804
  this.resourceID = resourceID;
20670
20805
  this.syncMode = syncMode;
20671
20806
  this.changeEventReceived = syncMode !== void 0 ? false : void 0;
20672
20807
  this.lastHeartbeatTime = Date.now();
20808
+ this.pollInterval = pollInterval;
20809
+ this.pollIntervalPinned = pollIntervalPinned;
20673
20810
  this.cancelled = false;
20674
20811
  }
20675
20812
  /**
@@ -20689,6 +20826,9 @@
20689
20826
  if (this.syncMode === SyncMode.RealtimePushOnly) {
20690
20827
  return this.resource.hasLocalChanges();
20691
20828
  }
20829
+ if (this.syncMode === SyncMode.Polling) {
20830
+ return Date.now() - this.lastHeartbeatTime >= this.pollInterval;
20831
+ }
20692
20832
  return this.syncMode !== SyncMode.Manual && (this.resource.hasLocalChanges() || (this.changeEventReceived ?? false));
20693
20833
  }
20694
20834
  /**
@@ -20702,7 +20842,8 @@
20702
20842
  if (this.syncMode === SyncMode.Manual) {
20703
20843
  return false;
20704
20844
  }
20705
- return Date.now() - this.lastHeartbeatTime >= heartbeatInterval;
20845
+ const interval = this.pollInterval > 0 ? this.pollInterval : heartbeatInterval;
20846
+ return Date.now() - this.lastHeartbeatTime >= interval;
20706
20847
  }
20707
20848
  /**
20708
20849
  * `updateHeartbeatTime` updates the last heartbeat time.
@@ -20781,6 +20922,16 @@
20781
20922
  }
20782
20923
  }
20783
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
+ }
20784
20935
  /**
20785
20936
  * `cancelWatchStream` cancels the watch stream.
20786
20937
  */
@@ -20815,7 +20966,7 @@
20815
20966
  };
20816
20967
  }
20817
20968
  const name = "@yorkie-js/sdk";
20818
- const version = "0.7.6";
20969
+ const version = "0.7.8";
20819
20970
  const pkg = {
20820
20971
  name,
20821
20972
  version
@@ -21098,6 +21249,7 @@
21098
21249
  SyncMode2["Realtime"] = "realtime";
21099
21250
  SyncMode2["RealtimePushOnly"] = "realtime-pushonly";
21100
21251
  SyncMode2["RealtimeSyncOff"] = "realtime-syncoff";
21252
+ SyncMode2["Polling"] = "polling";
21101
21253
  return SyncMode2;
21102
21254
  })(SyncMode || {});
21103
21255
  var ClientStatus = /* @__PURE__ */ ((ClientStatus2) => {
@@ -21110,6 +21262,7 @@
21110
21262
  ClientCondition2["WatchLoop"] = "WatchLoop";
21111
21263
  return ClientCondition2;
21112
21264
  })(ClientCondition || {});
21265
+ const DefaultPollingIntervalMs = 3e3;
21113
21266
  const DefaultClientOptions = {
21114
21267
  rpcAddr: "https://api.yorkie.dev",
21115
21268
  syncLoopDuration: 50,
@@ -21305,6 +21458,14 @@
21305
21458
  doc.setActor(this.id);
21306
21459
  doc.update((_, p) => p.set(opts.initialPresence || {}));
21307
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;
21308
21469
  return this.enqueueTask(async () => {
21309
21470
  try {
21310
21471
  const res = await this.rpcClient.attachDocument(
@@ -21334,10 +21495,12 @@
21334
21495
  this.reconnectStreamDelay,
21335
21496
  doc,
21336
21497
  res.documentId,
21337
- syncMode
21498
+ syncMode,
21499
+ pollInterval,
21500
+ pollIntervalPinned
21338
21501
  )
21339
21502
  );
21340
- if (syncMode !== "manual") {
21503
+ if (syncMode !== "manual" && syncMode !== "polling") {
21341
21504
  await this.runWatchLoop(doc.getKey());
21342
21505
  }
21343
21506
  logger.info(`[AD] c:"${this.getKey()}" attaches d:"${doc.getKey()}"`);
@@ -21353,6 +21516,7 @@
21353
21516
  }
21354
21517
  });
21355
21518
  }
21519
+ doc.clearHistory();
21356
21520
  return doc;
21357
21521
  } catch (err) {
21358
21522
  logger.error(`[AD] c:"${this.getKey()}" err :`, err);
@@ -21463,12 +21627,23 @@
21463
21627
  channel.setSessionID(res.sessionId);
21464
21628
  channel.updateSessionCount(Number(res.sessionCount), 0);
21465
21629
  channel.applyStatus(ChannelStatus.Attached);
21466
- 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;
21467
21640
  const attachment = new Attachment(
21468
21641
  this.reconnectStreamDelay,
21469
21642
  channel,
21470
21643
  res.sessionId,
21471
- syncMode
21644
+ syncMode,
21645
+ pollInterval,
21646
+ pollIntervalPinned
21472
21647
  );
21473
21648
  channel.subscribe("local-broadcast", (event) => {
21474
21649
  const { topic, payload, options } = event;
@@ -21544,9 +21719,17 @@
21544
21719
  return this.enqueueTask(task);
21545
21720
  }
21546
21721
  /**
21547
- * `changeSyncMode` changes the synchronization mode of the given document.
21722
+ * `changeSyncMode` changes the synchronization mode of the given resource.
21548
21723
  */
21549
- 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) {
21550
21733
  if (!this.isActive()) {
21551
21734
  throw new YorkieError(
21552
21735
  Code.ErrClientNotActivated,
@@ -21564,19 +21747,66 @@
21564
21747
  if (prevSyncMode === syncMode) {
21565
21748
  return doc;
21566
21749
  }
21567
- attachment.changeSyncMode(syncMode);
21568
- if (syncMode === "manual") {
21750
+ if (syncMode === "manual" || syncMode === "polling") {
21569
21751
  attachment.cancelWatchStream();
21570
- return doc;
21571
21752
  }
21753
+ attachment.changeSyncMode(syncMode);
21572
21754
  if (syncMode === "realtime") {
21573
21755
  attachment.changeEventReceived = true;
21574
21756
  }
21575
- 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();
21576
21762
  await this.runWatchLoop(doc.getKey());
21577
21763
  }
21578
21764
  return doc;
21579
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
+ }
21580
21810
  /**
21581
21811
  * `sync` implementation that handles both Document and Channel.
21582
21812
  */
@@ -22421,6 +22651,7 @@
22421
22651
  return doc;
22422
22652
  }
22423
22653
  doc.applyChangePack(respPack);
22654
+ attachment.updateHeartbeatTime();
22424
22655
  attachment.resource.publish([
22425
22656
  {
22426
22657
  type: DocEventType.SyncStatusChanged,
@@ -22531,8 +22762,11 @@
22531
22762
  function isCounter(value) {
22532
22763
  return typeof value === "object" && value !== null && value.type === "Counter" && typeof value.value === "object";
22533
22764
  }
22765
+ function isDedupCounter(value) {
22766
+ return typeof value === "object" && value !== null && value.type === "DedupCounter" && typeof value.value === "object" && typeof value.registers === "string";
22767
+ }
22534
22768
  function isObject(value) {
22535
- return typeof value === "object" && value !== null && !Array.isArray(value) && !isText(value) && !isTree(value) && !isInt(value) && !isLong(value) && !isDate(value) && !isBinData(value) && !isCounter(value);
22769
+ return typeof value === "object" && value !== null && !Array.isArray(value) && !isText(value) && !isTree(value) && !isInt(value) && !isLong(value) && !isDate(value) && !isBinData(value) && !isCounter(value) && !isDedupCounter(value);
22536
22770
  }
22537
22771
  function parse(yson) {
22538
22772
  try {
@@ -22548,6 +22782,12 @@
22548
22782
  }
22549
22783
  function preprocessYSON(yson) {
22550
22784
  let result = yson;
22785
+ result = result.replace(
22786
+ /DedupCounter\(Int\((-?\d+)\),"([^"]+)"\)/g,
22787
+ (_, value, registers) => {
22788
+ return `{"__yson_type":"DedupCounter","__yson_data":{"__yson_type":"Int","__yson_data":${value}},"__yson_registers":"${registers}"}`;
22789
+ }
22790
+ );
22551
22791
  result = result.replace(
22552
22792
  /Counter\((Int|Long)\((-?\d+)\)\)/g,
22553
22793
  (_, type, value) => {
@@ -22608,6 +22848,20 @@
22608
22848
  value: value.__yson_data
22609
22849
  };
22610
22850
  }
22851
+ if (value.__yson_type === "DedupCounter" && typeof value.__yson_data === "object" && typeof value.__yson_registers === "string") {
22852
+ const counterValue = postprocessValue(value.__yson_data);
22853
+ if (typeof counterValue === "object" && counterValue !== null && "type" in counterValue && counterValue.type === "Int") {
22854
+ return {
22855
+ type: "DedupCounter",
22856
+ value: counterValue,
22857
+ registers: value.__yson_registers
22858
+ };
22859
+ }
22860
+ throw new YorkieError(
22861
+ Code.ErrInvalidArgument,
22862
+ "DedupCounter must contain Int"
22863
+ );
22864
+ }
22611
22865
  if (value.__yson_type === "Counter" && typeof value.__yson_data === "object") {
22612
22866
  const counterValue = postprocessValue(value.__yson_data);
22613
22867
  if (typeof counterValue === "object" && counterValue !== null && "type" in counterValue && (counterValue.type === "Int" || counterValue.type === "Long")) {
@@ -22696,6 +22950,7 @@
22696
22950
  isBinData,
22697
22951
  isCounter,
22698
22952
  isDate,
22953
+ isDedupCounter,
22699
22954
  isInt,
22700
22955
  isLong,
22701
22956
  isObject,