@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.
@@ -358,7 +358,7 @@ function isScalarZeroValue(type, value) {
358
358
  }
359
359
  }
360
360
  const IMPLICIT$3 = 2;
361
- const unsafeLocal = Symbol.for("reflect unsafe local");
361
+ const unsafeLocal = /* @__PURE__ */ Symbol.for("reflect unsafe local");
362
362
  function unsafeOneofCase(target, oneof) {
363
363
  const c = target[oneof.localName].case;
364
364
  if (c === void 0) {
@@ -584,7 +584,7 @@ function convertObjectValues(obj, fn) {
584
584
  }
585
585
  return ret;
586
586
  }
587
- const tokenZeroMessageField = Symbol();
587
+ const tokenZeroMessageField = /* @__PURE__ */ Symbol();
588
588
  const messagePrototypes = /* @__PURE__ */ new WeakMap();
589
589
  function createZeroMessage(desc) {
590
590
  let msg;
@@ -686,7 +686,7 @@ class FieldError extends Error {
686
686
  function isFieldError(arg) {
687
687
  return arg instanceof Error && errorNames.includes(arg.name) && "field" in arg && typeof arg.field == "function";
688
688
  }
689
- const symbol = Symbol.for("@bufbuild/protobuf/text-encoding");
689
+ const symbol = /* @__PURE__ */ Symbol.for("@bufbuild/protobuf/text-encoding");
690
690
  function getTextEncoding() {
691
691
  if (globalThis[symbol] == void 0) {
692
692
  const te = new globalThis.TextEncoder();
@@ -4054,7 +4054,7 @@ function readScalarField(msg, field, json) {
4054
4054
  msg.set(field, scalarValue);
4055
4055
  }
4056
4056
  }
4057
- const tokenIgnoredUnknownEnum = Symbol();
4057
+ const tokenIgnoredUnknownEnum = /* @__PURE__ */ Symbol();
4058
4058
  function readEnum(desc, json, ignoreUnknownFields, nullAsZeroValue) {
4059
4059
  if (json === null) {
4060
4060
  if (desc.typeName == "google.protobuf.NullValue") {
@@ -4080,7 +4080,7 @@ function readEnum(desc, json, ignoreUnknownFields, nullAsZeroValue) {
4080
4080
  }
4081
4081
  throw new Error(`cannot decode ${desc} from JSON: ${formatVal(json)}`);
4082
4082
  }
4083
- const tokenNull = Symbol();
4083
+ const tokenNull = /* @__PURE__ */ Symbol();
4084
4084
  function scalarFromJson(field, json, nullAsZeroValue) {
4085
4085
  if (json === null) {
4086
4086
  if (nullAsZeroValue) {
@@ -5901,6 +5901,8 @@ class YorkieError extends Error {
5901
5901
  this.message = message;
5902
5902
  this.toString = () => `[code=${this.code}]: ${this.message}`;
5903
5903
  }
5904
+ code;
5905
+ message;
5904
5906
  name = "YorkieError";
5905
5907
  stack;
5906
5908
  }
@@ -11130,6 +11132,32 @@ class IndexTreeNode {
11130
11132
  }
11131
11133
  actualRight.push(child);
11132
11134
  }
11135
+ if (versionVector) {
11136
+ const movedToLeft = [];
11137
+ const remaining = [];
11138
+ let boundaryReached = false;
11139
+ for (const child of actualRight) {
11140
+ if (!boundaryReached) {
11141
+ if (child.insPrevID !== void 0 && !child.isText) {
11142
+ remaining.push(child);
11143
+ continue;
11144
+ }
11145
+ const actorID = child.id.getCreatedAt().getActorID();
11146
+ const knownLamport = versionVector.get(actorID);
11147
+ if (knownLamport === void 0 || knownLamport < child.id.getCreatedAt().getLamport()) {
11148
+ movedToLeft.push(child);
11149
+ continue;
11150
+ }
11151
+ }
11152
+ boundaryReached = true;
11153
+ remaining.push(child);
11154
+ }
11155
+ if (movedToLeft.length > 0) {
11156
+ left.push(...movedToLeft);
11157
+ actualRight.length = 0;
11158
+ actualRight.push(...remaining);
11159
+ }
11160
+ }
11133
11161
  this._children = left;
11134
11162
  clone._children = actualRight;
11135
11163
  this.visibleSize = this._children.reduce(
@@ -11906,8 +11934,14 @@ class CRDTTreeNode extends IndexTreeNode {
11906
11934
  split.insPrevID = this.id;
11907
11935
  if (this.insNextID) {
11908
11936
  const insNext = tree.findFloorNode(this.insNextID);
11909
- insNext.insPrevID = split.id;
11910
11937
  split.insNextID = this.insNextID;
11938
+ if (insNext) {
11939
+ insNext.insPrevID = split.id;
11940
+ if (!this.isText && insNext.parent && !insNext.isRemoved && insNext.parent !== split.parent && split.allChildren.length === 0) {
11941
+ split.parent.detachChild(split);
11942
+ insNext.parent.insertBefore(split, insNext);
11943
+ }
11944
+ }
11911
11945
  }
11912
11946
  this.insNextID = split.id;
11913
11947
  tree.registerNode(split);
@@ -12131,7 +12165,7 @@ class CRDTTree extends CRDTElement {
12131
12165
  * given node, advancing past element-type split siblings that the editing
12132
12166
  * client did not know about (not in versionVector).
12133
12167
  */
12134
- advancePastUnknownSplitSiblings(node, versionVector) {
12168
+ advancePastUnknownSplitSiblings(node, versionVector, relaxParentCheck = false, skipActorID) {
12135
12169
  if (!versionVector || !node) {
12136
12170
  return node;
12137
12171
  }
@@ -12141,10 +12175,13 @@ class CRDTTree extends CRDTElement {
12141
12175
  if (!next || next.isText) {
12142
12176
  break;
12143
12177
  }
12144
- if (next.parent !== current.parent) {
12178
+ if (!relaxParentCheck && next.parent !== current.parent) {
12145
12179
  break;
12146
12180
  }
12147
12181
  const actorID = next.id.getCreatedAt().getActorID();
12182
+ if (skipActorID !== void 0 && actorID === skipActorID) {
12183
+ break;
12184
+ }
12148
12185
  const knownLamport = versionVector.get(actorID);
12149
12186
  if (knownLamport !== void 0 && knownLamport >= next.id.getCreatedAt().getLamport()) {
12150
12187
  break;
@@ -12485,6 +12522,25 @@ class CRDTTree extends CRDTElement {
12485
12522
  addDataSizes(diff, diffTo, diffFrom);
12486
12523
  const fromLeft = fromLeftRaw !== fromParent ? this.advancePastUnknownSplitSiblings(fromLeftRaw, versionVector) : fromLeftRaw;
12487
12524
  const toLeft = toLeftRaw !== toParent ? this.advancePastUnknownSplitSiblings(toLeftRaw, versionVector) : toLeftRaw;
12525
+ let collectFromParent = fromParent;
12526
+ let collectFromLeft = fromLeft;
12527
+ if (fromLeft !== fromParent && fromParent !== toParent) {
12528
+ let current = fromLeft;
12529
+ while (current.insNextID) {
12530
+ const next = this.findFloorNode(current.insNextID);
12531
+ if (!next || next.isText) {
12532
+ break;
12533
+ }
12534
+ if (next.parent && next.parent === toParent) {
12535
+ if (toLeft !== toParent) {
12536
+ collectFromLeft = next;
12537
+ collectFromParent = toParent;
12538
+ }
12539
+ break;
12540
+ }
12541
+ current = next;
12542
+ }
12543
+ }
12488
12544
  const fromIdx = this.toIndex(fromParent, fromLeft);
12489
12545
  const fromPath = this.toPath(fromParent, fromLeft);
12490
12546
  const nodesToBeRemoved = [];
@@ -12493,8 +12549,8 @@ class CRDTTree extends CRDTElement {
12493
12549
  const toBeMergedNodes = [];
12494
12550
  const preTombstoned = /* @__PURE__ */ new Set();
12495
12551
  this.traverseInPosRange(
12496
- fromParent,
12497
- fromLeft,
12552
+ collectFromParent,
12553
+ collectFromLeft,
12498
12554
  toParent,
12499
12555
  toLeft,
12500
12556
  ([node, tokenType], ended) => {
@@ -12542,6 +12598,7 @@ class CRDTTree extends CRDTElement {
12542
12598
  tokensToBeRemoved,
12543
12599
  editedAt
12544
12600
  );
12601
+ const mergeLevel = toBeMergedNodes.length;
12545
12602
  const pairs = [];
12546
12603
  for (const node of nodesToBeRemoved) {
12547
12604
  if (node.remove(editedAt)) {
@@ -12598,9 +12655,20 @@ class CRDTTree extends CRDTElement {
12598
12655
  let parent = fromParent;
12599
12656
  let left = fromLeft;
12600
12657
  while (splitCount < splitLevel) {
12658
+ if (left !== parent) {
12659
+ left = this.advancePastUnknownSplitSiblings(
12660
+ left,
12661
+ versionVector,
12662
+ true,
12663
+ editedAt.getActorID()
12664
+ );
12665
+ if (left.parent && left.parent !== parent) {
12666
+ parent = left.parent;
12667
+ }
12668
+ }
12601
12669
  parent.split(
12602
12670
  this,
12603
- parent.findOffset(left, true) + 1,
12671
+ left !== parent ? parent.findOffset(left, true) + 1 : 0,
12604
12672
  issueTimeTicket,
12605
12673
  versionVector
12606
12674
  );
@@ -12657,7 +12725,15 @@ class CRDTTree extends CRDTElement {
12657
12725
  }
12658
12726
  }
12659
12727
  }
12660
- return [changes, pairs, diff, nodesToBeRemoved, fromIdx];
12728
+ return [
12729
+ changes,
12730
+ pairs,
12731
+ diff,
12732
+ nodesToBeRemoved,
12733
+ fromIdx,
12734
+ mergeLevel,
12735
+ preTombstoned
12736
+ ];
12661
12737
  }
12662
12738
  /**
12663
12739
  * `editT` edits the given range with the given value.
@@ -13098,11 +13174,35 @@ class CRDTTree extends CRDTElement {
13098
13174
  return [prev, prev.isText ? TokenType.Text : TokenType.End];
13099
13175
  }
13100
13176
  }
13101
- function clearRemovedAt(node) {
13102
- traverseAll(node, (n) => {
13177
+ function cloneAndDropPreTombstoned(node, preTombstoned) {
13178
+ const clone = node.deepcopy();
13179
+ filterChildren(clone, preTombstoned);
13180
+ traverseAll(clone, (n) => {
13103
13181
  n.removedAt = void 0;
13104
- n.visibleSize = n.totalSize;
13182
+ if (n.isText) {
13183
+ n.visibleSize = n.value.length;
13184
+ n.totalSize = n.value.length;
13185
+ return;
13186
+ }
13187
+ let size = 0;
13188
+ for (const child of n._children) size += child.paddedSize();
13189
+ n.visibleSize = size;
13190
+ n.totalSize = size;
13105
13191
  });
13192
+ return clone;
13193
+ }
13194
+ function filterChildren(node, preTombstoned) {
13195
+ const all = node._children;
13196
+ if (!all) return;
13197
+ const kept = [];
13198
+ for (const child of all) {
13199
+ if (preTombstoned.has(child.id.toIDString())) {
13200
+ continue;
13201
+ }
13202
+ filterChildren(child, preTombstoned);
13203
+ kept.push(child);
13204
+ }
13205
+ node._children = kept;
13106
13206
  }
13107
13207
  class TreeEditOperation extends Operation {
13108
13208
  fromPos;
@@ -13174,7 +13274,15 @@ class TreeEditOperation extends Operation {
13174
13274
  this.toPos = tree.findPos(this.toIdx);
13175
13275
  }
13176
13276
  }
13177
- const [changes, pairs, diff, removedNodes, preEditFromIdx] = tree.edit(
13277
+ const [
13278
+ changes,
13279
+ pairs,
13280
+ diff,
13281
+ removedNodes,
13282
+ preEditFromIdx,
13283
+ mergeLevel,
13284
+ preTombstoned
13285
+ ] = tree.edit(
13178
13286
  [this.fromPos, this.toPos],
13179
13287
  this.contents?.map((content) => content.deepcopy()),
13180
13288
  this.splitLevel,
@@ -13207,10 +13315,16 @@ class TreeEditOperation extends Operation {
13207
13315
  );
13208
13316
  this.lastToIdx = preEditFromIdx + removedSize;
13209
13317
  let reverseOp;
13210
- const isPureL1Split = this.splitLevel === 1 && !this.contents?.length && removedNodes.length === 0;
13318
+ const isPureSplit = this.splitLevel > 0 && !this.contents?.length && removedNodes.length === 0;
13211
13319
  if (this.splitLevel === 0) {
13212
- reverseOp = this.toReverseOperation(tree, removedNodes, preEditFromIdx);
13213
- } else if (isPureL1Split) {
13320
+ reverseOp = this.toReverseOperation(
13321
+ tree,
13322
+ removedNodes,
13323
+ preEditFromIdx,
13324
+ preTombstoned,
13325
+ mergeLevel
13326
+ );
13327
+ } else if (isPureSplit) {
13214
13328
  reverseOp = this.toSplitReverseOperation(tree, preEditFromIdx);
13215
13329
  }
13216
13330
  root.acc(diff);
@@ -13247,7 +13361,7 @@ class TreeEditOperation extends Operation {
13247
13361
  * @param removedNodes - Nodes that were removed by this edit
13248
13362
  * @param preEditFromIdx - The from index captured BEFORE the edit
13249
13363
  */
13250
- toReverseOperation(tree, removedNodes, preEditFromIdx) {
13364
+ toReverseOperation(tree, removedNodes, preEditFromIdx, preTombstoned, mergeLevel) {
13251
13365
  if (this.redoSplitLevel !== void 0 && this.redoSplitLevel > 0) {
13252
13366
  const splitRedoFromPos = tree.findPos(preEditFromIdx);
13253
13367
  const splitRedoOp = TreeEditOperation.create(
@@ -13266,19 +13380,36 @@ class TreeEditOperation extends Operation {
13266
13380
  );
13267
13381
  return splitRedoOp;
13268
13382
  }
13383
+ if (mergeLevel && mergeLevel > 0) {
13384
+ const splitFromPos = tree.findPos(preEditFromIdx);
13385
+ const splitUndoOp = TreeEditOperation.create(
13386
+ this.getParentCreatedAt(),
13387
+ splitFromPos,
13388
+ splitFromPos,
13389
+ void 0,
13390
+ // no inserted content — split creates boundaries
13391
+ mergeLevel,
13392
+ // splitLevel = number of merged boundaries
13393
+ void 0,
13394
+ // executedAt assigned at undo time
13395
+ true,
13396
+ // isUndoOp
13397
+ preEditFromIdx,
13398
+ preEditFromIdx
13399
+ );
13400
+ return splitUndoOp;
13401
+ }
13269
13402
  const insertedContentSize = this.contents ? this.contents.reduce((sum, node) => sum + node.paddedSize(), 0) : 0;
13270
13403
  const maxNeededIdx = preEditFromIdx + insertedContentSize;
13271
13404
  if (maxNeededIdx > tree.getSize()) {
13272
13405
  return void 0;
13273
13406
  }
13274
13407
  const topLevelRemoved = removedNodes.filter(
13275
- (node) => !node.parent || !removedNodes.includes(node.parent)
13408
+ (node) => !preTombstoned.has(node.id.toIDString()) && (!node.parent || !removedNodes.includes(node.parent))
13276
13409
  );
13277
- const reverseContents = topLevelRemoved.length > 0 ? topLevelRemoved.map((n) => {
13278
- const clone = n.deepcopy();
13279
- clearRemovedAt(clone);
13280
- return clone;
13281
- }) : void 0;
13410
+ const reverseContents = topLevelRemoved.length > 0 ? topLevelRemoved.map(
13411
+ (n) => cloneAndDropPreTombstoned(n, preTombstoned)
13412
+ ) : void 0;
13282
13413
  const reverseFromPos = tree.findPos(preEditFromIdx);
13283
13414
  let reverseToPos;
13284
13415
  if (insertedContentSize > 0) {
@@ -20097,6 +20228,11 @@ class Document {
20097
20228
  }
20098
20229
  ]);
20099
20230
  }
20231
+ /**
20232
+ * `clearHistory` flushes both undo and redo stacks. This is used
20233
+ * after applying a snapshot or initialRoot so that setup operations
20234
+ * are not reachable via undo.
20235
+ */
20100
20236
  clearHistory() {
20101
20237
  this.internalHistory.clearRedo();
20102
20238
  this.internalHistory.clearUndo();
@@ -20556,10 +20692,7 @@ class Document {
20556
20692
  }
20557
20693
  const ops = isUndo ? this.internalHistory.popUndo() : this.internalHistory.popRedo();
20558
20694
  if (!ops) {
20559
- throw new YorkieError(
20560
- Code.ErrRefused,
20561
- `There is no operation to be ${isUndo ? "undone" : "redone"}`
20562
- );
20695
+ return;
20563
20696
  }
20564
20697
  this.ensureClone();
20565
20698
  const ctx = ChangeContext.create(
@@ -20652,6 +20785,8 @@ class Attachment {
20652
20785
  syncMode;
20653
20786
  changeEventReceived;
20654
20787
  lastHeartbeatTime;
20788
+ pollInterval;
20789
+ pollIntervalPinned;
20655
20790
  reconnectStreamDelay;
20656
20791
  cancelled;
20657
20792
  watchStream;
@@ -20659,13 +20794,15 @@ class Attachment {
20659
20794
  watchAbortController;
20660
20795
  syncPromise;
20661
20796
  _detaching = false;
20662
- constructor(reconnectStreamDelay, resource, resourceID, syncMode) {
20797
+ constructor(reconnectStreamDelay, resource, resourceID, syncMode, pollInterval = 0, pollIntervalPinned = false) {
20663
20798
  this.reconnectStreamDelay = reconnectStreamDelay;
20664
20799
  this.resource = resource;
20665
20800
  this.resourceID = resourceID;
20666
20801
  this.syncMode = syncMode;
20667
20802
  this.changeEventReceived = syncMode !== void 0 ? false : void 0;
20668
20803
  this.lastHeartbeatTime = Date.now();
20804
+ this.pollInterval = pollInterval;
20805
+ this.pollIntervalPinned = pollIntervalPinned;
20669
20806
  this.cancelled = false;
20670
20807
  }
20671
20808
  /**
@@ -20685,6 +20822,9 @@ class Attachment {
20685
20822
  if (this.syncMode === SyncMode.RealtimePushOnly) {
20686
20823
  return this.resource.hasLocalChanges();
20687
20824
  }
20825
+ if (this.syncMode === SyncMode.Polling) {
20826
+ return Date.now() - this.lastHeartbeatTime >= this.pollInterval;
20827
+ }
20688
20828
  return this.syncMode !== SyncMode.Manual && (this.resource.hasLocalChanges() || (this.changeEventReceived ?? false));
20689
20829
  }
20690
20830
  /**
@@ -20698,7 +20838,8 @@ class Attachment {
20698
20838
  if (this.syncMode === SyncMode.Manual) {
20699
20839
  return false;
20700
20840
  }
20701
- return Date.now() - this.lastHeartbeatTime >= heartbeatInterval;
20841
+ const interval = this.pollInterval > 0 ? this.pollInterval : heartbeatInterval;
20842
+ return Date.now() - this.lastHeartbeatTime >= interval;
20702
20843
  }
20703
20844
  /**
20704
20845
  * `updateHeartbeatTime` updates the last heartbeat time.
@@ -20777,6 +20918,16 @@ class Attachment {
20777
20918
  }
20778
20919
  }
20779
20920
  }
20921
+ /**
20922
+ * `resetCancelled` clears the cancelled flag so the watch loop can run again
20923
+ * after a previous cancellation (e.g., after changeSyncMode back to Realtime).
20924
+ * Caller must invoke `runWatchLoop` immediately after to claim the stream slot;
20925
+ * `doLoop`'s `if (this.watchStream)` guard prevents double-stream creation if a
20926
+ * delayed `onDisconnect` callback from the previously-cancelled stream races.
20927
+ */
20928
+ resetCancelled() {
20929
+ this.cancelled = false;
20930
+ }
20780
20931
  /**
20781
20932
  * `cancelWatchStream` cancels the watch stream.
20782
20933
  */
@@ -20811,7 +20962,7 @@ function createAuthInterceptor(apiKey, token) {
20811
20962
  };
20812
20963
  }
20813
20964
  const name = "@yorkie-js/sdk";
20814
- const version = "0.7.6";
20965
+ const version = "0.7.8";
20815
20966
  const pkg = {
20816
20967
  name,
20817
20968
  version
@@ -21094,6 +21245,7 @@ var SyncMode = /* @__PURE__ */ ((SyncMode2) => {
21094
21245
  SyncMode2["Realtime"] = "realtime";
21095
21246
  SyncMode2["RealtimePushOnly"] = "realtime-pushonly";
21096
21247
  SyncMode2["RealtimeSyncOff"] = "realtime-syncoff";
21248
+ SyncMode2["Polling"] = "polling";
21097
21249
  return SyncMode2;
21098
21250
  })(SyncMode || {});
21099
21251
  var ClientStatus = /* @__PURE__ */ ((ClientStatus2) => {
@@ -21106,6 +21258,7 @@ var ClientCondition = /* @__PURE__ */ ((ClientCondition2) => {
21106
21258
  ClientCondition2["WatchLoop"] = "WatchLoop";
21107
21259
  return ClientCondition2;
21108
21260
  })(ClientCondition || {});
21261
+ const DefaultPollingIntervalMs = 3e3;
21109
21262
  const DefaultClientOptions = {
21110
21263
  rpcAddr: "https://api.yorkie.dev",
21111
21264
  syncLoopDuration: 50,
@@ -21301,6 +21454,14 @@ class Client {
21301
21454
  doc.setActor(this.id);
21302
21455
  doc.update((_, p) => p.set(opts.initialPresence || {}));
21303
21456
  const syncMode = opts.syncMode ?? "realtime";
21457
+ if (opts.documentPollInterval !== void 0 && opts.documentPollInterval <= 0) {
21458
+ throw new YorkieError(
21459
+ Code.ErrInvalidArgument,
21460
+ "documentPollInterval must be greater than 0"
21461
+ );
21462
+ }
21463
+ const pollIntervalPinned = opts.documentPollInterval !== void 0;
21464
+ const pollInterval = pollIntervalPinned ? opts.documentPollInterval : syncMode === "polling" ? DefaultPollingIntervalMs : 0;
21304
21465
  return this.enqueueTask(async () => {
21305
21466
  try {
21306
21467
  const res = await this.rpcClient.attachDocument(
@@ -21330,10 +21491,12 @@ class Client {
21330
21491
  this.reconnectStreamDelay,
21331
21492
  doc,
21332
21493
  res.documentId,
21333
- syncMode
21494
+ syncMode,
21495
+ pollInterval,
21496
+ pollIntervalPinned
21334
21497
  )
21335
21498
  );
21336
- if (syncMode !== "manual") {
21499
+ if (syncMode !== "manual" && syncMode !== "polling") {
21337
21500
  await this.runWatchLoop(doc.getKey());
21338
21501
  }
21339
21502
  logger.info(`[AD] c:"${this.getKey()}" attaches d:"${doc.getKey()}"`);
@@ -21349,6 +21512,7 @@ class Client {
21349
21512
  }
21350
21513
  });
21351
21514
  }
21515
+ doc.clearHistory();
21352
21516
  return doc;
21353
21517
  } catch (err) {
21354
21518
  logger.error(`[AD] c:"${this.getKey()}" err :`, err);
@@ -21459,12 +21623,23 @@ class Client {
21459
21623
  channel.setSessionID(res.sessionId);
21460
21624
  channel.updateSessionCount(Number(res.sessionCount), 0);
21461
21625
  channel.applyStatus(ChannelStatus.Attached);
21462
- const syncMode = opts.isRealtime !== false ? "realtime" : "manual";
21626
+ const syncMode = opts.syncMode ?? "realtime";
21627
+ this.assertValidChannelSyncMode(syncMode);
21628
+ if (opts.channelHeartbeatInterval !== void 0 && opts.channelHeartbeatInterval <= 0) {
21629
+ throw new YorkieError(
21630
+ Code.ErrInvalidArgument,
21631
+ "channelHeartbeatInterval must be greater than 0"
21632
+ );
21633
+ }
21634
+ const pollIntervalPinned = opts.channelHeartbeatInterval !== void 0;
21635
+ const pollInterval = pollIntervalPinned ? opts.channelHeartbeatInterval : syncMode === "polling" ? DefaultPollingIntervalMs : this.channelHeartbeatInterval;
21463
21636
  const attachment = new Attachment(
21464
21637
  this.reconnectStreamDelay,
21465
21638
  channel,
21466
21639
  res.sessionId,
21467
- syncMode
21640
+ syncMode,
21641
+ pollInterval,
21642
+ pollIntervalPinned
21468
21643
  );
21469
21644
  channel.subscribe("local-broadcast", (event) => {
21470
21645
  const { topic, payload, options } = event;
@@ -21540,9 +21715,17 @@ class Client {
21540
21715
  return this.enqueueTask(task);
21541
21716
  }
21542
21717
  /**
21543
- * `changeSyncMode` changes the synchronization mode of the given document.
21718
+ * `changeSyncMode` changes the synchronization mode of the given resource.
21544
21719
  */
21545
- async changeSyncMode(doc, syncMode) {
21720
+ async changeSyncMode(resource, syncMode) {
21721
+ return this.enqueueTask(async () => {
21722
+ if (resource instanceof Channel2) {
21723
+ return this.changeChannelSyncMode(resource, syncMode);
21724
+ }
21725
+ return this.changeDocumentSyncMode(resource, syncMode);
21726
+ });
21727
+ }
21728
+ async changeDocumentSyncMode(doc, syncMode) {
21546
21729
  if (!this.isActive()) {
21547
21730
  throw new YorkieError(
21548
21731
  Code.ErrClientNotActivated,
@@ -21560,19 +21743,66 @@ class Client {
21560
21743
  if (prevSyncMode === syncMode) {
21561
21744
  return doc;
21562
21745
  }
21563
- attachment.changeSyncMode(syncMode);
21564
- if (syncMode === "manual") {
21746
+ if (syncMode === "manual" || syncMode === "polling") {
21565
21747
  attachment.cancelWatchStream();
21566
- return doc;
21567
21748
  }
21749
+ attachment.changeSyncMode(syncMode);
21568
21750
  if (syncMode === "realtime") {
21569
21751
  attachment.changeEventReceived = true;
21570
21752
  }
21571
- if (prevSyncMode === "manual") {
21753
+ if (!attachment.pollIntervalPinned) {
21754
+ attachment.pollInterval = syncMode === "polling" ? DefaultPollingIntervalMs : 0;
21755
+ }
21756
+ if ((prevSyncMode === "manual" || prevSyncMode === "polling") && syncMode !== "manual" && syncMode !== "polling") {
21757
+ attachment.resetCancelled();
21572
21758
  await this.runWatchLoop(doc.getKey());
21573
21759
  }
21574
21760
  return doc;
21575
21761
  }
21762
+ /**
21763
+ * `assertValidChannelSyncMode` rejects sync modes that are not valid for
21764
+ * channels. `RealtimePushOnly` and `RealtimeSyncOff` are document-only.
21765
+ */
21766
+ assertValidChannelSyncMode(syncMode) {
21767
+ if (syncMode !== "manual" && syncMode !== "realtime" && syncMode !== "polling") {
21768
+ throw new YorkieError(
21769
+ Code.ErrInvalidArgument,
21770
+ `invalid channel sync mode: ${syncMode}`
21771
+ );
21772
+ }
21773
+ }
21774
+ async changeChannelSyncMode(channel, syncMode) {
21775
+ if (!this.isActive()) {
21776
+ throw new YorkieError(
21777
+ Code.ErrClientNotActivated,
21778
+ `${this.key} is not active`
21779
+ );
21780
+ }
21781
+ const attachment = this.attachmentMap.get(channel.getKey());
21782
+ if (!attachment) {
21783
+ throw new YorkieError(
21784
+ Code.ErrNotAttached,
21785
+ `${channel.getKey()} is not attached`
21786
+ );
21787
+ }
21788
+ const prevSyncMode = attachment.syncMode;
21789
+ if (prevSyncMode === syncMode) {
21790
+ return channel;
21791
+ }
21792
+ this.assertValidChannelSyncMode(syncMode);
21793
+ if (prevSyncMode === "realtime") {
21794
+ attachment.cancelWatchStream();
21795
+ }
21796
+ attachment.changeSyncMode(syncMode);
21797
+ if (!attachment.pollIntervalPinned) {
21798
+ attachment.pollInterval = syncMode === "polling" ? DefaultPollingIntervalMs : syncMode === "realtime" ? this.channelHeartbeatInterval : 0;
21799
+ }
21800
+ if (syncMode === "realtime") {
21801
+ attachment.resetCancelled();
21802
+ await this.runWatchLoop(channel.getKey());
21803
+ }
21804
+ return channel;
21805
+ }
21576
21806
  /**
21577
21807
  * `sync` implementation that handles both Document and Channel.
21578
21808
  */
@@ -22417,6 +22647,7 @@ class Client {
22417
22647
  return doc;
22418
22648
  }
22419
22649
  doc.applyChangePack(respPack);
22650
+ attachment.updateHeartbeatTime();
22420
22651
  attachment.resource.publish([
22421
22652
  {
22422
22653
  type: DocEventType.SyncStatusChanged,
@@ -22527,8 +22758,11 @@ function isBinData(value) {
22527
22758
  function isCounter(value) {
22528
22759
  return typeof value === "object" && value !== null && value.type === "Counter" && typeof value.value === "object";
22529
22760
  }
22761
+ function isDedupCounter(value) {
22762
+ return typeof value === "object" && value !== null && value.type === "DedupCounter" && typeof value.value === "object" && typeof value.registers === "string";
22763
+ }
22530
22764
  function isObject(value) {
22531
- return typeof value === "object" && value !== null && !Array.isArray(value) && !isText(value) && !isTree(value) && !isInt(value) && !isLong(value) && !isDate(value) && !isBinData(value) && !isCounter(value);
22765
+ 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);
22532
22766
  }
22533
22767
  function parse(yson) {
22534
22768
  try {
@@ -22544,6 +22778,12 @@ function parse(yson) {
22544
22778
  }
22545
22779
  function preprocessYSON(yson) {
22546
22780
  let result = yson;
22781
+ result = result.replace(
22782
+ /DedupCounter\(Int\((-?\d+)\),"([^"]+)"\)/g,
22783
+ (_, value, registers) => {
22784
+ return `{"__yson_type":"DedupCounter","__yson_data":{"__yson_type":"Int","__yson_data":${value}},"__yson_registers":"${registers}"}`;
22785
+ }
22786
+ );
22547
22787
  result = result.replace(
22548
22788
  /Counter\((Int|Long)\((-?\d+)\)\)/g,
22549
22789
  (_, type, value) => {
@@ -22604,6 +22844,20 @@ function postprocessValue(value) {
22604
22844
  value: value.__yson_data
22605
22845
  };
22606
22846
  }
22847
+ if (value.__yson_type === "DedupCounter" && typeof value.__yson_data === "object" && typeof value.__yson_registers === "string") {
22848
+ const counterValue = postprocessValue(value.__yson_data);
22849
+ if (typeof counterValue === "object" && counterValue !== null && "type" in counterValue && counterValue.type === "Int") {
22850
+ return {
22851
+ type: "DedupCounter",
22852
+ value: counterValue,
22853
+ registers: value.__yson_registers
22854
+ };
22855
+ }
22856
+ throw new YorkieError(
22857
+ Code.ErrInvalidArgument,
22858
+ "DedupCounter must contain Int"
22859
+ );
22860
+ }
22607
22861
  if (value.__yson_type === "Counter" && typeof value.__yson_data === "object") {
22608
22862
  const counterValue = postprocessValue(value.__yson_data);
22609
22863
  if (typeof counterValue === "object" && counterValue !== null && "type" in counterValue && (counterValue.type === "Int" || counterValue.type === "Long")) {
@@ -22692,6 +22946,7 @@ const YSON = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty
22692
22946
  isBinData,
22693
22947
  isCounter,
22694
22948
  isDate,
22949
+ isDedupCounter,
22695
22950
  isInt,
22696
22951
  isLong,
22697
22952
  isObject,