@yorkie-js/react 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.
@@ -360,7 +360,7 @@ function isScalarZeroValue(type, value) {
360
360
  }
361
361
  }
362
362
  const IMPLICIT$3 = 2;
363
- const unsafeLocal = Symbol.for("reflect unsafe local");
363
+ const unsafeLocal = /* @__PURE__ */ Symbol.for("reflect unsafe local");
364
364
  function unsafeOneofCase(target, oneof) {
365
365
  const c = target[oneof.localName].case;
366
366
  if (c === void 0) {
@@ -586,7 +586,7 @@ function convertObjectValues(obj, fn) {
586
586
  }
587
587
  return ret;
588
588
  }
589
- const tokenZeroMessageField = Symbol();
589
+ const tokenZeroMessageField = /* @__PURE__ */ Symbol();
590
590
  const messagePrototypes = /* @__PURE__ */ new WeakMap();
591
591
  function createZeroMessage(desc) {
592
592
  let msg;
@@ -688,7 +688,7 @@ class FieldError extends Error {
688
688
  function isFieldError(arg) {
689
689
  return arg instanceof Error && errorNames.includes(arg.name) && "field" in arg && typeof arg.field == "function";
690
690
  }
691
- const symbol = Symbol.for("@bufbuild/protobuf/text-encoding");
691
+ const symbol = /* @__PURE__ */ Symbol.for("@bufbuild/protobuf/text-encoding");
692
692
  function getTextEncoding() {
693
693
  if (globalThis[symbol] == void 0) {
694
694
  const te = new globalThis.TextEncoder();
@@ -4056,7 +4056,7 @@ function readScalarField(msg, field, json) {
4056
4056
  msg.set(field, scalarValue);
4057
4057
  }
4058
4058
  }
4059
- const tokenIgnoredUnknownEnum = Symbol();
4059
+ const tokenIgnoredUnknownEnum = /* @__PURE__ */ Symbol();
4060
4060
  function readEnum(desc, json, ignoreUnknownFields, nullAsZeroValue) {
4061
4061
  if (json === null) {
4062
4062
  if (desc.typeName == "google.protobuf.NullValue") {
@@ -4082,7 +4082,7 @@ function readEnum(desc, json, ignoreUnknownFields, nullAsZeroValue) {
4082
4082
  }
4083
4083
  throw new Error(`cannot decode ${desc} from JSON: ${formatVal(json)}`);
4084
4084
  }
4085
- const tokenNull = Symbol();
4085
+ const tokenNull = /* @__PURE__ */ Symbol();
4086
4086
  function scalarFromJson(field, json, nullAsZeroValue) {
4087
4087
  if (json === null) {
4088
4088
  if (nullAsZeroValue) {
@@ -5903,6 +5903,8 @@ class YorkieError extends Error {
5903
5903
  this.message = message;
5904
5904
  this.toString = () => `[code=${this.code}]: ${this.message}`;
5905
5905
  }
5906
+ code;
5907
+ message;
5906
5908
  name = "YorkieError";
5907
5909
  stack;
5908
5910
  }
@@ -11132,6 +11134,32 @@ class IndexTreeNode {
11132
11134
  }
11133
11135
  actualRight.push(child);
11134
11136
  }
11137
+ if (versionVector) {
11138
+ const movedToLeft = [];
11139
+ const remaining = [];
11140
+ let boundaryReached = false;
11141
+ for (const child of actualRight) {
11142
+ if (!boundaryReached) {
11143
+ if (child.insPrevID !== void 0 && !child.isText) {
11144
+ remaining.push(child);
11145
+ continue;
11146
+ }
11147
+ const actorID = child.id.getCreatedAt().getActorID();
11148
+ const knownLamport = versionVector.get(actorID);
11149
+ if (knownLamport === void 0 || knownLamport < child.id.getCreatedAt().getLamport()) {
11150
+ movedToLeft.push(child);
11151
+ continue;
11152
+ }
11153
+ }
11154
+ boundaryReached = true;
11155
+ remaining.push(child);
11156
+ }
11157
+ if (movedToLeft.length > 0) {
11158
+ left.push(...movedToLeft);
11159
+ actualRight.length = 0;
11160
+ actualRight.push(...remaining);
11161
+ }
11162
+ }
11135
11163
  this._children = left;
11136
11164
  clone._children = actualRight;
11137
11165
  this.visibleSize = this._children.reduce(
@@ -11908,8 +11936,14 @@ class CRDTTreeNode extends IndexTreeNode {
11908
11936
  split.insPrevID = this.id;
11909
11937
  if (this.insNextID) {
11910
11938
  const insNext = tree.findFloorNode(this.insNextID);
11911
- insNext.insPrevID = split.id;
11912
11939
  split.insNextID = this.insNextID;
11940
+ if (insNext) {
11941
+ insNext.insPrevID = split.id;
11942
+ if (!this.isText && insNext.parent && !insNext.isRemoved && insNext.parent !== split.parent && split.allChildren.length === 0) {
11943
+ split.parent.detachChild(split);
11944
+ insNext.parent.insertBefore(split, insNext);
11945
+ }
11946
+ }
11913
11947
  }
11914
11948
  this.insNextID = split.id;
11915
11949
  tree.registerNode(split);
@@ -12133,7 +12167,7 @@ class CRDTTree extends CRDTElement {
12133
12167
  * given node, advancing past element-type split siblings that the editing
12134
12168
  * client did not know about (not in versionVector).
12135
12169
  */
12136
- advancePastUnknownSplitSiblings(node, versionVector) {
12170
+ advancePastUnknownSplitSiblings(node, versionVector, relaxParentCheck = false, skipActorID) {
12137
12171
  if (!versionVector || !node) {
12138
12172
  return node;
12139
12173
  }
@@ -12143,10 +12177,13 @@ class CRDTTree extends CRDTElement {
12143
12177
  if (!next || next.isText) {
12144
12178
  break;
12145
12179
  }
12146
- if (next.parent !== current.parent) {
12180
+ if (!relaxParentCheck && next.parent !== current.parent) {
12147
12181
  break;
12148
12182
  }
12149
12183
  const actorID = next.id.getCreatedAt().getActorID();
12184
+ if (skipActorID !== void 0 && actorID === skipActorID) {
12185
+ break;
12186
+ }
12150
12187
  const knownLamport = versionVector.get(actorID);
12151
12188
  if (knownLamport !== void 0 && knownLamport >= next.id.getCreatedAt().getLamport()) {
12152
12189
  break;
@@ -12487,6 +12524,25 @@ class CRDTTree extends CRDTElement {
12487
12524
  addDataSizes(diff, diffTo, diffFrom);
12488
12525
  const fromLeft = fromLeftRaw !== fromParent ? this.advancePastUnknownSplitSiblings(fromLeftRaw, versionVector) : fromLeftRaw;
12489
12526
  const toLeft = toLeftRaw !== toParent ? this.advancePastUnknownSplitSiblings(toLeftRaw, versionVector) : toLeftRaw;
12527
+ let collectFromParent = fromParent;
12528
+ let collectFromLeft = fromLeft;
12529
+ if (fromLeft !== fromParent && fromParent !== toParent) {
12530
+ let current = fromLeft;
12531
+ while (current.insNextID) {
12532
+ const next = this.findFloorNode(current.insNextID);
12533
+ if (!next || next.isText) {
12534
+ break;
12535
+ }
12536
+ if (next.parent && next.parent === toParent) {
12537
+ if (toLeft !== toParent) {
12538
+ collectFromLeft = next;
12539
+ collectFromParent = toParent;
12540
+ }
12541
+ break;
12542
+ }
12543
+ current = next;
12544
+ }
12545
+ }
12490
12546
  const fromIdx = this.toIndex(fromParent, fromLeft);
12491
12547
  const fromPath = this.toPath(fromParent, fromLeft);
12492
12548
  const nodesToBeRemoved = [];
@@ -12495,8 +12551,8 @@ class CRDTTree extends CRDTElement {
12495
12551
  const toBeMergedNodes = [];
12496
12552
  const preTombstoned = /* @__PURE__ */ new Set();
12497
12553
  this.traverseInPosRange(
12498
- fromParent,
12499
- fromLeft,
12554
+ collectFromParent,
12555
+ collectFromLeft,
12500
12556
  toParent,
12501
12557
  toLeft,
12502
12558
  ([node, tokenType], ended) => {
@@ -12544,6 +12600,7 @@ class CRDTTree extends CRDTElement {
12544
12600
  tokensToBeRemoved,
12545
12601
  editedAt
12546
12602
  );
12603
+ const mergeLevel = toBeMergedNodes.length;
12547
12604
  const pairs = [];
12548
12605
  for (const node of nodesToBeRemoved) {
12549
12606
  if (node.remove(editedAt)) {
@@ -12600,9 +12657,20 @@ class CRDTTree extends CRDTElement {
12600
12657
  let parent = fromParent;
12601
12658
  let left = fromLeft;
12602
12659
  while (splitCount < splitLevel) {
12660
+ if (left !== parent) {
12661
+ left = this.advancePastUnknownSplitSiblings(
12662
+ left,
12663
+ versionVector,
12664
+ true,
12665
+ editedAt.getActorID()
12666
+ );
12667
+ if (left.parent && left.parent !== parent) {
12668
+ parent = left.parent;
12669
+ }
12670
+ }
12603
12671
  parent.split(
12604
12672
  this,
12605
- parent.findOffset(left, true) + 1,
12673
+ left !== parent ? parent.findOffset(left, true) + 1 : 0,
12606
12674
  issueTimeTicket,
12607
12675
  versionVector
12608
12676
  );
@@ -12659,7 +12727,15 @@ class CRDTTree extends CRDTElement {
12659
12727
  }
12660
12728
  }
12661
12729
  }
12662
- return [changes, pairs, diff, nodesToBeRemoved, fromIdx];
12730
+ return [
12731
+ changes,
12732
+ pairs,
12733
+ diff,
12734
+ nodesToBeRemoved,
12735
+ fromIdx,
12736
+ mergeLevel,
12737
+ preTombstoned
12738
+ ];
12663
12739
  }
12664
12740
  /**
12665
12741
  * `editT` edits the given range with the given value.
@@ -13100,11 +13176,35 @@ class CRDTTree extends CRDTElement {
13100
13176
  return [prev, prev.isText ? TokenType.Text : TokenType.End];
13101
13177
  }
13102
13178
  }
13103
- function clearRemovedAt(node) {
13104
- traverseAll(node, (n) => {
13179
+ function cloneAndDropPreTombstoned(node, preTombstoned) {
13180
+ const clone = node.deepcopy();
13181
+ filterChildren(clone, preTombstoned);
13182
+ traverseAll(clone, (n) => {
13105
13183
  n.removedAt = void 0;
13106
- n.visibleSize = n.totalSize;
13184
+ if (n.isText) {
13185
+ n.visibleSize = n.value.length;
13186
+ n.totalSize = n.value.length;
13187
+ return;
13188
+ }
13189
+ let size = 0;
13190
+ for (const child of n._children) size += child.paddedSize();
13191
+ n.visibleSize = size;
13192
+ n.totalSize = size;
13107
13193
  });
13194
+ return clone;
13195
+ }
13196
+ function filterChildren(node, preTombstoned) {
13197
+ const all = node._children;
13198
+ if (!all) return;
13199
+ const kept = [];
13200
+ for (const child of all) {
13201
+ if (preTombstoned.has(child.id.toIDString())) {
13202
+ continue;
13203
+ }
13204
+ filterChildren(child, preTombstoned);
13205
+ kept.push(child);
13206
+ }
13207
+ node._children = kept;
13108
13208
  }
13109
13209
  class TreeEditOperation extends Operation {
13110
13210
  fromPos;
@@ -13176,7 +13276,15 @@ class TreeEditOperation extends Operation {
13176
13276
  this.toPos = tree.findPos(this.toIdx);
13177
13277
  }
13178
13278
  }
13179
- const [changes, pairs, diff, removedNodes, preEditFromIdx] = tree.edit(
13279
+ const [
13280
+ changes,
13281
+ pairs,
13282
+ diff,
13283
+ removedNodes,
13284
+ preEditFromIdx,
13285
+ mergeLevel,
13286
+ preTombstoned
13287
+ ] = tree.edit(
13180
13288
  [this.fromPos, this.toPos],
13181
13289
  this.contents?.map((content) => content.deepcopy()),
13182
13290
  this.splitLevel,
@@ -13209,10 +13317,16 @@ class TreeEditOperation extends Operation {
13209
13317
  );
13210
13318
  this.lastToIdx = preEditFromIdx + removedSize;
13211
13319
  let reverseOp;
13212
- const isPureL1Split = this.splitLevel === 1 && !this.contents?.length && removedNodes.length === 0;
13320
+ const isPureSplit = this.splitLevel > 0 && !this.contents?.length && removedNodes.length === 0;
13213
13321
  if (this.splitLevel === 0) {
13214
- reverseOp = this.toReverseOperation(tree, removedNodes, preEditFromIdx);
13215
- } else if (isPureL1Split) {
13322
+ reverseOp = this.toReverseOperation(
13323
+ tree,
13324
+ removedNodes,
13325
+ preEditFromIdx,
13326
+ preTombstoned,
13327
+ mergeLevel
13328
+ );
13329
+ } else if (isPureSplit) {
13216
13330
  reverseOp = this.toSplitReverseOperation(tree, preEditFromIdx);
13217
13331
  }
13218
13332
  root.acc(diff);
@@ -13249,7 +13363,7 @@ class TreeEditOperation extends Operation {
13249
13363
  * @param removedNodes - Nodes that were removed by this edit
13250
13364
  * @param preEditFromIdx - The from index captured BEFORE the edit
13251
13365
  */
13252
- toReverseOperation(tree, removedNodes, preEditFromIdx) {
13366
+ toReverseOperation(tree, removedNodes, preEditFromIdx, preTombstoned, mergeLevel) {
13253
13367
  if (this.redoSplitLevel !== void 0 && this.redoSplitLevel > 0) {
13254
13368
  const splitRedoFromPos = tree.findPos(preEditFromIdx);
13255
13369
  const splitRedoOp = TreeEditOperation.create(
@@ -13268,19 +13382,36 @@ class TreeEditOperation extends Operation {
13268
13382
  );
13269
13383
  return splitRedoOp;
13270
13384
  }
13385
+ if (mergeLevel && mergeLevel > 0) {
13386
+ const splitFromPos = tree.findPos(preEditFromIdx);
13387
+ const splitUndoOp = TreeEditOperation.create(
13388
+ this.getParentCreatedAt(),
13389
+ splitFromPos,
13390
+ splitFromPos,
13391
+ void 0,
13392
+ // no inserted content — split creates boundaries
13393
+ mergeLevel,
13394
+ // splitLevel = number of merged boundaries
13395
+ void 0,
13396
+ // executedAt assigned at undo time
13397
+ true,
13398
+ // isUndoOp
13399
+ preEditFromIdx,
13400
+ preEditFromIdx
13401
+ );
13402
+ return splitUndoOp;
13403
+ }
13271
13404
  const insertedContentSize = this.contents ? this.contents.reduce((sum, node) => sum + node.paddedSize(), 0) : 0;
13272
13405
  const maxNeededIdx = preEditFromIdx + insertedContentSize;
13273
13406
  if (maxNeededIdx > tree.getSize()) {
13274
13407
  return void 0;
13275
13408
  }
13276
13409
  const topLevelRemoved = removedNodes.filter(
13277
- (node) => !node.parent || !removedNodes.includes(node.parent)
13410
+ (node) => !preTombstoned.has(node.id.toIDString()) && (!node.parent || !removedNodes.includes(node.parent))
13278
13411
  );
13279
- const reverseContents = topLevelRemoved.length > 0 ? topLevelRemoved.map((n) => {
13280
- const clone = n.deepcopy();
13281
- clearRemovedAt(clone);
13282
- return clone;
13283
- }) : void 0;
13412
+ const reverseContents = topLevelRemoved.length > 0 ? topLevelRemoved.map(
13413
+ (n) => cloneAndDropPreTombstoned(n, preTombstoned)
13414
+ ) : void 0;
13284
13415
  const reverseFromPos = tree.findPos(preEditFromIdx);
13285
13416
  let reverseToPos;
13286
13417
  if (insertedContentSize > 0) {
@@ -20094,6 +20225,11 @@ class Document {
20094
20225
  }
20095
20226
  ]);
20096
20227
  }
20228
+ /**
20229
+ * `clearHistory` flushes both undo and redo stacks. This is used
20230
+ * after applying a snapshot or initialRoot so that setup operations
20231
+ * are not reachable via undo.
20232
+ */
20097
20233
  clearHistory() {
20098
20234
  this.internalHistory.clearRedo();
20099
20235
  this.internalHistory.clearUndo();
@@ -20553,10 +20689,7 @@ class Document {
20553
20689
  }
20554
20690
  const ops = isUndo ? this.internalHistory.popUndo() : this.internalHistory.popRedo();
20555
20691
  if (!ops) {
20556
- throw new YorkieError(
20557
- Code.ErrRefused,
20558
- `There is no operation to be ${isUndo ? "undone" : "redone"}`
20559
- );
20692
+ return;
20560
20693
  }
20561
20694
  this.ensureClone();
20562
20695
  const ctx = ChangeContext.create(
@@ -20649,6 +20782,8 @@ class Attachment {
20649
20782
  syncMode;
20650
20783
  changeEventReceived;
20651
20784
  lastHeartbeatTime;
20785
+ pollInterval;
20786
+ pollIntervalPinned;
20652
20787
  reconnectStreamDelay;
20653
20788
  cancelled;
20654
20789
  watchStream;
@@ -20656,13 +20791,15 @@ class Attachment {
20656
20791
  watchAbortController;
20657
20792
  syncPromise;
20658
20793
  _detaching = false;
20659
- constructor(reconnectStreamDelay, resource, resourceID, syncMode) {
20794
+ constructor(reconnectStreamDelay, resource, resourceID, syncMode, pollInterval = 0, pollIntervalPinned = false) {
20660
20795
  this.reconnectStreamDelay = reconnectStreamDelay;
20661
20796
  this.resource = resource;
20662
20797
  this.resourceID = resourceID;
20663
20798
  this.syncMode = syncMode;
20664
20799
  this.changeEventReceived = syncMode !== void 0 ? false : void 0;
20665
20800
  this.lastHeartbeatTime = Date.now();
20801
+ this.pollInterval = pollInterval;
20802
+ this.pollIntervalPinned = pollIntervalPinned;
20666
20803
  this.cancelled = false;
20667
20804
  }
20668
20805
  /**
@@ -20682,6 +20819,9 @@ class Attachment {
20682
20819
  if (this.syncMode === SyncMode.RealtimePushOnly) {
20683
20820
  return this.resource.hasLocalChanges();
20684
20821
  }
20822
+ if (this.syncMode === SyncMode.Polling) {
20823
+ return Date.now() - this.lastHeartbeatTime >= this.pollInterval;
20824
+ }
20685
20825
  return this.syncMode !== SyncMode.Manual && (this.resource.hasLocalChanges() || (this.changeEventReceived ?? false));
20686
20826
  }
20687
20827
  /**
@@ -20695,7 +20835,8 @@ class Attachment {
20695
20835
  if (this.syncMode === SyncMode.Manual) {
20696
20836
  return false;
20697
20837
  }
20698
- return Date.now() - this.lastHeartbeatTime >= heartbeatInterval;
20838
+ const interval = this.pollInterval > 0 ? this.pollInterval : heartbeatInterval;
20839
+ return Date.now() - this.lastHeartbeatTime >= interval;
20699
20840
  }
20700
20841
  /**
20701
20842
  * `updateHeartbeatTime` updates the last heartbeat time.
@@ -20774,6 +20915,16 @@ class Attachment {
20774
20915
  }
20775
20916
  }
20776
20917
  }
20918
+ /**
20919
+ * `resetCancelled` clears the cancelled flag so the watch loop can run again
20920
+ * after a previous cancellation (e.g., after changeSyncMode back to Realtime).
20921
+ * Caller must invoke `runWatchLoop` immediately after to claim the stream slot;
20922
+ * `doLoop`'s `if (this.watchStream)` guard prevents double-stream creation if a
20923
+ * delayed `onDisconnect` callback from the previously-cancelled stream races.
20924
+ */
20925
+ resetCancelled() {
20926
+ this.cancelled = false;
20927
+ }
20777
20928
  /**
20778
20929
  * `cancelWatchStream` cancels the watch stream.
20779
20930
  */
@@ -20808,7 +20959,7 @@ function createAuthInterceptor(apiKey, token) {
20808
20959
  };
20809
20960
  }
20810
20961
  const name$1 = "@yorkie-js/sdk";
20811
- const version$1 = "0.7.6";
20962
+ const version$1 = "0.7.8";
20812
20963
  const pkg$1 = {
20813
20964
  name: name$1,
20814
20965
  version: version$1
@@ -21091,8 +21242,10 @@ var SyncMode = /* @__PURE__ */ ((SyncMode2) => {
21091
21242
  SyncMode2["Realtime"] = "realtime";
21092
21243
  SyncMode2["RealtimePushOnly"] = "realtime-pushonly";
21093
21244
  SyncMode2["RealtimeSyncOff"] = "realtime-syncoff";
21245
+ SyncMode2["Polling"] = "polling";
21094
21246
  return SyncMode2;
21095
21247
  })(SyncMode || {});
21248
+ const DefaultPollingIntervalMs = 3e3;
21096
21249
  const DefaultClientOptions = {
21097
21250
  rpcAddr: "https://api.yorkie.dev",
21098
21251
  syncLoopDuration: 50,
@@ -21288,6 +21441,14 @@ class Client {
21288
21441
  doc.setActor(this.id);
21289
21442
  doc.update((_, p) => p.set(opts.initialPresence || {}));
21290
21443
  const syncMode = opts.syncMode ?? "realtime";
21444
+ if (opts.documentPollInterval !== void 0 && opts.documentPollInterval <= 0) {
21445
+ throw new YorkieError(
21446
+ Code.ErrInvalidArgument,
21447
+ "documentPollInterval must be greater than 0"
21448
+ );
21449
+ }
21450
+ const pollIntervalPinned = opts.documentPollInterval !== void 0;
21451
+ const pollInterval = pollIntervalPinned ? opts.documentPollInterval : syncMode === "polling" ? DefaultPollingIntervalMs : 0;
21291
21452
  return this.enqueueTask(async () => {
21292
21453
  try {
21293
21454
  const res = await this.rpcClient.attachDocument(
@@ -21317,10 +21478,12 @@ class Client {
21317
21478
  this.reconnectStreamDelay,
21318
21479
  doc,
21319
21480
  res.documentId,
21320
- syncMode
21481
+ syncMode,
21482
+ pollInterval,
21483
+ pollIntervalPinned
21321
21484
  )
21322
21485
  );
21323
- if (syncMode !== "manual") {
21486
+ if (syncMode !== "manual" && syncMode !== "polling") {
21324
21487
  await this.runWatchLoop(doc.getKey());
21325
21488
  }
21326
21489
  logger.info(`[AD] c:"${this.getKey()}" attaches d:"${doc.getKey()}"`);
@@ -21336,6 +21499,7 @@ class Client {
21336
21499
  }
21337
21500
  });
21338
21501
  }
21502
+ doc.clearHistory();
21339
21503
  return doc;
21340
21504
  } catch (err) {
21341
21505
  logger.error(`[AD] c:"${this.getKey()}" err :`, err);
@@ -21446,12 +21610,23 @@ class Client {
21446
21610
  channel.setSessionID(res.sessionId);
21447
21611
  channel.updateSessionCount(Number(res.sessionCount), 0);
21448
21612
  channel.applyStatus(ChannelStatus.Attached);
21449
- const syncMode = opts.isRealtime !== false ? "realtime" : "manual";
21613
+ const syncMode = opts.syncMode ?? "realtime";
21614
+ this.assertValidChannelSyncMode(syncMode);
21615
+ if (opts.channelHeartbeatInterval !== void 0 && opts.channelHeartbeatInterval <= 0) {
21616
+ throw new YorkieError(
21617
+ Code.ErrInvalidArgument,
21618
+ "channelHeartbeatInterval must be greater than 0"
21619
+ );
21620
+ }
21621
+ const pollIntervalPinned = opts.channelHeartbeatInterval !== void 0;
21622
+ const pollInterval = pollIntervalPinned ? opts.channelHeartbeatInterval : syncMode === "polling" ? DefaultPollingIntervalMs : this.channelHeartbeatInterval;
21450
21623
  const attachment = new Attachment(
21451
21624
  this.reconnectStreamDelay,
21452
21625
  channel,
21453
21626
  res.sessionId,
21454
- syncMode
21627
+ syncMode,
21628
+ pollInterval,
21629
+ pollIntervalPinned
21455
21630
  );
21456
21631
  channel.subscribe("local-broadcast", (event) => {
21457
21632
  const { topic, payload, options } = event;
@@ -21527,9 +21702,17 @@ class Client {
21527
21702
  return this.enqueueTask(task);
21528
21703
  }
21529
21704
  /**
21530
- * `changeSyncMode` changes the synchronization mode of the given document.
21705
+ * `changeSyncMode` changes the synchronization mode of the given resource.
21531
21706
  */
21532
- async changeSyncMode(doc, syncMode) {
21707
+ async changeSyncMode(resource, syncMode) {
21708
+ return this.enqueueTask(async () => {
21709
+ if (resource instanceof Channel2) {
21710
+ return this.changeChannelSyncMode(resource, syncMode);
21711
+ }
21712
+ return this.changeDocumentSyncMode(resource, syncMode);
21713
+ });
21714
+ }
21715
+ async changeDocumentSyncMode(doc, syncMode) {
21533
21716
  if (!this.isActive()) {
21534
21717
  throw new YorkieError(
21535
21718
  Code.ErrClientNotActivated,
@@ -21547,19 +21730,66 @@ class Client {
21547
21730
  if (prevSyncMode === syncMode) {
21548
21731
  return doc;
21549
21732
  }
21550
- attachment.changeSyncMode(syncMode);
21551
- if (syncMode === "manual") {
21733
+ if (syncMode === "manual" || syncMode === "polling") {
21552
21734
  attachment.cancelWatchStream();
21553
- return doc;
21554
21735
  }
21736
+ attachment.changeSyncMode(syncMode);
21555
21737
  if (syncMode === "realtime") {
21556
21738
  attachment.changeEventReceived = true;
21557
21739
  }
21558
- if (prevSyncMode === "manual") {
21740
+ if (!attachment.pollIntervalPinned) {
21741
+ attachment.pollInterval = syncMode === "polling" ? DefaultPollingIntervalMs : 0;
21742
+ }
21743
+ if ((prevSyncMode === "manual" || prevSyncMode === "polling") && syncMode !== "manual" && syncMode !== "polling") {
21744
+ attachment.resetCancelled();
21559
21745
  await this.runWatchLoop(doc.getKey());
21560
21746
  }
21561
21747
  return doc;
21562
21748
  }
21749
+ /**
21750
+ * `assertValidChannelSyncMode` rejects sync modes that are not valid for
21751
+ * channels. `RealtimePushOnly` and `RealtimeSyncOff` are document-only.
21752
+ */
21753
+ assertValidChannelSyncMode(syncMode) {
21754
+ if (syncMode !== "manual" && syncMode !== "realtime" && syncMode !== "polling") {
21755
+ throw new YorkieError(
21756
+ Code.ErrInvalidArgument,
21757
+ `invalid channel sync mode: ${syncMode}`
21758
+ );
21759
+ }
21760
+ }
21761
+ async changeChannelSyncMode(channel, syncMode) {
21762
+ if (!this.isActive()) {
21763
+ throw new YorkieError(
21764
+ Code.ErrClientNotActivated,
21765
+ `${this.key} is not active`
21766
+ );
21767
+ }
21768
+ const attachment = this.attachmentMap.get(channel.getKey());
21769
+ if (!attachment) {
21770
+ throw new YorkieError(
21771
+ Code.ErrNotAttached,
21772
+ `${channel.getKey()} is not attached`
21773
+ );
21774
+ }
21775
+ const prevSyncMode = attachment.syncMode;
21776
+ if (prevSyncMode === syncMode) {
21777
+ return channel;
21778
+ }
21779
+ this.assertValidChannelSyncMode(syncMode);
21780
+ if (prevSyncMode === "realtime") {
21781
+ attachment.cancelWatchStream();
21782
+ }
21783
+ attachment.changeSyncMode(syncMode);
21784
+ if (!attachment.pollIntervalPinned) {
21785
+ attachment.pollInterval = syncMode === "polling" ? DefaultPollingIntervalMs : syncMode === "realtime" ? this.channelHeartbeatInterval : 0;
21786
+ }
21787
+ if (syncMode === "realtime") {
21788
+ attachment.resetCancelled();
21789
+ await this.runWatchLoop(channel.getKey());
21790
+ }
21791
+ return channel;
21792
+ }
21563
21793
  /**
21564
21794
  * `sync` implementation that handles both Document and Channel.
21565
21795
  */
@@ -22404,6 +22634,7 @@ class Client {
22404
22634
  return doc;
22405
22635
  }
22406
22636
  doc.applyChangePack(respPack);
22637
+ attachment.updateHeartbeatTime();
22407
22638
  attachment.resource.publish([
22408
22639
  {
22409
22640
  type: DocEventType.SyncStatusChanged,
@@ -22514,8 +22745,11 @@ function isBinData(value) {
22514
22745
  function isCounter(value) {
22515
22746
  return typeof value === "object" && value !== null && value.type === "Counter" && typeof value.value === "object";
22516
22747
  }
22748
+ function isDedupCounter(value) {
22749
+ return typeof value === "object" && value !== null && value.type === "DedupCounter" && typeof value.value === "object" && typeof value.registers === "string";
22750
+ }
22517
22751
  function isObject(value) {
22518
- return typeof value === "object" && value !== null && !Array.isArray(value) && !isText(value) && !isTree(value) && !isInt(value) && !isLong(value) && !isDate(value) && !isBinData(value) && !isCounter(value);
22752
+ 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);
22519
22753
  }
22520
22754
  function parse(yson) {
22521
22755
  try {
@@ -22531,6 +22765,12 @@ function parse(yson) {
22531
22765
  }
22532
22766
  function preprocessYSON(yson) {
22533
22767
  let result = yson;
22768
+ result = result.replace(
22769
+ /DedupCounter\(Int\((-?\d+)\),"([^"]+)"\)/g,
22770
+ (_, value, registers) => {
22771
+ return `{"__yson_type":"DedupCounter","__yson_data":{"__yson_type":"Int","__yson_data":${value}},"__yson_registers":"${registers}"}`;
22772
+ }
22773
+ );
22534
22774
  result = result.replace(
22535
22775
  /Counter\((Int|Long)\((-?\d+)\)\)/g,
22536
22776
  (_, type, value) => {
@@ -22591,6 +22831,20 @@ function postprocessValue(value) {
22591
22831
  value: value.__yson_data
22592
22832
  };
22593
22833
  }
22834
+ if (value.__yson_type === "DedupCounter" && typeof value.__yson_data === "object" && typeof value.__yson_registers === "string") {
22835
+ const counterValue = postprocessValue(value.__yson_data);
22836
+ if (typeof counterValue === "object" && counterValue !== null && "type" in counterValue && counterValue.type === "Int") {
22837
+ return {
22838
+ type: "DedupCounter",
22839
+ value: counterValue,
22840
+ registers: value.__yson_registers
22841
+ };
22842
+ }
22843
+ throw new YorkieError(
22844
+ Code.ErrInvalidArgument,
22845
+ "DedupCounter must contain Int"
22846
+ );
22847
+ }
22594
22848
  if (value.__yson_type === "Counter" && typeof value.__yson_data === "object") {
22595
22849
  const counterValue = postprocessValue(value.__yson_data);
22596
22850
  if (typeof counterValue === "object" && counterValue !== null && "type" in counterValue && (counterValue.type === "Int" || counterValue.type === "Long")) {
@@ -22679,6 +22933,7 @@ const YSON = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty
22679
22933
  isBinData,
22680
22934
  isCounter,
22681
22935
  isDate,
22936
+ isDedupCounter,
22682
22937
  isInt,
22683
22938
  isLong,
22684
22939
  isObject,
@@ -22705,7 +22960,7 @@ if (typeof globalThis !== "undefined") {
22705
22960
  };
22706
22961
  }
22707
22962
  const name = "@yorkie-js/react";
22708
- const version = "0.7.6";
22963
+ const version = "0.7.8";
22709
22964
  const pkg = {
22710
22965
  name,
22711
22966
  version
@@ -23161,7 +23416,7 @@ function createChannelStore(initialState) {
23161
23416
  const ChannelContext = createContext(
23162
23417
  void 0
23163
23418
  );
23164
- function useYorkieChannel(client, clientLoading, clientError, channelKey, isRealtime, channelStore) {
23419
+ function useYorkieChannel(client, clientLoading, clientError, channelKey, syncMode, channelStore) {
23165
23420
  const channelRef = useRef(void 0);
23166
23421
  const [didMount, setDidMount] = useState(false);
23167
23422
  useEffect(() => {
@@ -23190,7 +23445,7 @@ function useYorkieChannel(client, clientLoading, clientError, channelKey, isReal
23190
23445
  }));
23191
23446
  try {
23192
23447
  const newChannel = new Channel2(channelKey);
23193
- await client.attach(newChannel, { isRealtime });
23448
+ await client.attach(newChannel, { syncMode });
23194
23449
  channelRef.current = newChannel;
23195
23450
  unsubscribe = newChannel.subscribe(() => {
23196
23451
  channelStore.setState((state) => ({
@@ -23228,12 +23483,13 @@ function useYorkieChannel(client, clientLoading, clientError, channelKey, isReal
23228
23483
  }
23229
23484
  detachChannel();
23230
23485
  };
23231
- }, [client, clientLoading, clientError, channelKey, isRealtime, didMount]);
23486
+ }, [client, clientLoading, clientError, channelKey, syncMode, didMount]);
23232
23487
  }
23233
23488
  const ChannelProvider = ({
23234
23489
  children,
23235
23490
  channelKey,
23236
- isRealtime = true
23491
+ syncMode,
23492
+ isRealtime
23237
23493
  }) => {
23238
23494
  const { client, loading: clientLoading, error: clientError } = useYorkie();
23239
23495
  const channelStoreRef = useRef(
@@ -23248,12 +23504,13 @@ const ChannelProvider = ({
23248
23504
  });
23249
23505
  }
23250
23506
  const channelStore = channelStoreRef.current;
23507
+ const resolvedSyncMode = syncMode ?? (isRealtime === false ? SyncMode.Manual : SyncMode.Realtime);
23251
23508
  useYorkieChannel(
23252
23509
  client,
23253
23510
  clientLoading,
23254
23511
  clientError,
23255
23512
  channelKey,
23256
- isRealtime,
23513
+ resolvedSyncMode,
23257
23514
  channelStore
23258
23515
  );
23259
23516
  return /* @__PURE__ */ jsx(ChannelContext.Provider, { value: channelStore, children });
@@ -23280,6 +23537,7 @@ export {
23280
23537
  ChannelProvider,
23281
23538
  Counter,
23282
23539
  DocumentProvider,
23540
+ SyncMode,
23283
23541
  Text,
23284
23542
  Tree,
23285
23543
  YorkieProvider,