@yorkie-js/react 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.
package/README.md CHANGED
@@ -8,7 +8,7 @@ To get started using Yorkie React SDK, see: https://yorkie.dev/docs/getting-star
8
8
 
9
9
  ## Contributing
10
10
 
11
- See [CONTRIBUTING](CONTRIBUTING.md) for details on submitting patches and the contribution workflow.
11
+ See [CONTRIBUTING](../../CONTRIBUTING.md) for details on submitting patches and the contribution workflow.
12
12
 
13
13
  ## Contributors ✨
14
14
 
@@ -10,6 +10,7 @@ import { Presence } from '@yorkie-js/sdk';
10
10
  import { PropsWithChildren } from 'react';
11
11
  import { RevisionSummary } from '@yorkie-js/sdk';
12
12
  import { StreamConnectionStatus } from '@yorkie-js/sdk';
13
+ import { SyncMode } from '@yorkie-js/sdk';
13
14
  import { Text as Text_2 } from '@yorkie-js/sdk';
14
15
  import { Tree } from '@yorkie-js/sdk';
15
16
 
@@ -19,12 +20,17 @@ import { Tree } from '@yorkie-js/sdk';
19
20
  *
20
21
  * @example
21
22
  * ```tsx
23
+ * import { ChannelProvider, SyncMode } from '@yorkie-js/react';
24
+ *
22
25
  * <YorkieProvider apiKey="..." rpcAddr="...">
23
- * <ChannelProvider channelKey="room-123" isRealtime={true}>
26
+ * <ChannelProvider channelKey="room-123" syncMode={SyncMode.Realtime}>
24
27
  * <ChatRoom />
25
28
  * </ChannelProvider>
26
29
  * </YorkieProvider>
27
30
  * ```
31
+ *
32
+ * The boolean `isRealtime` prop is also accepted as a shortcut for
33
+ * the Realtime/Manual choice (`true` → Realtime, `false` → Manual).
28
34
  */
29
35
  export declare const ChannelProvider: React.FC<ChannelProviderProps>;
30
36
 
@@ -37,10 +43,24 @@ declare type ChannelProviderProps = PropsWithChildren<{
37
43
  */
38
44
  channelKey: string;
39
45
  /**
40
- * `isRealtime` determines the synchronization mode.
41
- * - true: Realtime mode (automatic updates via watch stream)
42
- * - false: Manual mode (requires manual sync)
43
- * @default true
46
+ * `syncMode` selects how the channel keeps presence in sync with the server.
47
+ * - `SyncMode.Realtime` (default): open a watch stream and run the
48
+ * heartbeat. Required to receive broadcast events.
49
+ * - `SyncMode.Polling`: heartbeat-only. No watch stream is opened.
50
+ * Recommended for large channels where broadcast is not needed.
51
+ * - `SyncMode.Manual`: no automatic activity.
52
+ *
53
+ * If `isRealtime` is also set, `syncMode` wins.
54
+ */
55
+ syncMode?: SyncMode;
56
+ /**
57
+ * `isRealtime` is a convenience prop covering only Realtime/Manual.
58
+ * - `true`: equivalent to `syncMode={SyncMode.Realtime}`.
59
+ * - `false`: equivalent to `syncMode={SyncMode.Manual}`.
60
+ *
61
+ * Use `syncMode` directly when you want `SyncMode.Polling`, which this
62
+ * boolean prop cannot express. When neither prop is set, the channel
63
+ * falls back to `SyncMode.Realtime`.
44
64
  */
45
65
  isRealtime?: boolean;
46
66
  }>;
@@ -92,6 +112,8 @@ export { RevisionSummary }
92
112
  */
93
113
  export declare function shallowEqual<T>(valueA: T, valueB: T): boolean;
94
114
 
115
+ export { SyncMode }
116
+
95
117
  export { Text_2 as Text }
96
118
 
97
119
  export { Tree }
@@ -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
  }
@@ -12532,8 +12534,10 @@ class CRDTTree extends CRDTElement {
12532
12534
  break;
12533
12535
  }
12534
12536
  if (next.parent && next.parent === toParent) {
12535
- collectFromLeft = next;
12536
- collectFromParent = toParent;
12537
+ if (toLeft !== toParent) {
12538
+ collectFromLeft = next;
12539
+ collectFromParent = toParent;
12540
+ }
12537
12541
  break;
12538
12542
  }
12539
12543
  current = next;
@@ -12596,6 +12600,7 @@ class CRDTTree extends CRDTElement {
12596
12600
  tokensToBeRemoved,
12597
12601
  editedAt
12598
12602
  );
12603
+ const mergeLevel = toBeMergedNodes.length;
12599
12604
  const pairs = [];
12600
12605
  for (const node of nodesToBeRemoved) {
12601
12606
  if (node.remove(editedAt)) {
@@ -12722,7 +12727,15 @@ class CRDTTree extends CRDTElement {
12722
12727
  }
12723
12728
  }
12724
12729
  }
12725
- return [changes, pairs, diff, nodesToBeRemoved, fromIdx];
12730
+ return [
12731
+ changes,
12732
+ pairs,
12733
+ diff,
12734
+ nodesToBeRemoved,
12735
+ fromIdx,
12736
+ mergeLevel,
12737
+ preTombstoned
12738
+ ];
12726
12739
  }
12727
12740
  /**
12728
12741
  * `editT` edits the given range with the given value.
@@ -13163,11 +13176,35 @@ class CRDTTree extends CRDTElement {
13163
13176
  return [prev, prev.isText ? TokenType.Text : TokenType.End];
13164
13177
  }
13165
13178
  }
13166
- function clearRemovedAt(node) {
13167
- traverseAll(node, (n) => {
13179
+ function cloneAndDropPreTombstoned(node, preTombstoned) {
13180
+ const clone = node.deepcopy();
13181
+ filterChildren(clone, preTombstoned);
13182
+ traverseAll(clone, (n) => {
13168
13183
  n.removedAt = void 0;
13169
- 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;
13170
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;
13171
13208
  }
13172
13209
  class TreeEditOperation extends Operation {
13173
13210
  fromPos;
@@ -13239,7 +13276,15 @@ class TreeEditOperation extends Operation {
13239
13276
  this.toPos = tree.findPos(this.toIdx);
13240
13277
  }
13241
13278
  }
13242
- 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(
13243
13288
  [this.fromPos, this.toPos],
13244
13289
  this.contents?.map((content) => content.deepcopy()),
13245
13290
  this.splitLevel,
@@ -13274,7 +13319,13 @@ class TreeEditOperation extends Operation {
13274
13319
  let reverseOp;
13275
13320
  const isPureSplit = this.splitLevel > 0 && !this.contents?.length && removedNodes.length === 0;
13276
13321
  if (this.splitLevel === 0) {
13277
- reverseOp = this.toReverseOperation(tree, removedNodes, preEditFromIdx);
13322
+ reverseOp = this.toReverseOperation(
13323
+ tree,
13324
+ removedNodes,
13325
+ preEditFromIdx,
13326
+ preTombstoned,
13327
+ mergeLevel
13328
+ );
13278
13329
  } else if (isPureSplit) {
13279
13330
  reverseOp = this.toSplitReverseOperation(tree, preEditFromIdx);
13280
13331
  }
@@ -13312,7 +13363,7 @@ class TreeEditOperation extends Operation {
13312
13363
  * @param removedNodes - Nodes that were removed by this edit
13313
13364
  * @param preEditFromIdx - The from index captured BEFORE the edit
13314
13365
  */
13315
- toReverseOperation(tree, removedNodes, preEditFromIdx) {
13366
+ toReverseOperation(tree, removedNodes, preEditFromIdx, preTombstoned, mergeLevel) {
13316
13367
  if (this.redoSplitLevel !== void 0 && this.redoSplitLevel > 0) {
13317
13368
  const splitRedoFromPos = tree.findPos(preEditFromIdx);
13318
13369
  const splitRedoOp = TreeEditOperation.create(
@@ -13331,19 +13382,36 @@ class TreeEditOperation extends Operation {
13331
13382
  );
13332
13383
  return splitRedoOp;
13333
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
+ }
13334
13404
  const insertedContentSize = this.contents ? this.contents.reduce((sum, node) => sum + node.paddedSize(), 0) : 0;
13335
13405
  const maxNeededIdx = preEditFromIdx + insertedContentSize;
13336
13406
  if (maxNeededIdx > tree.getSize()) {
13337
13407
  return void 0;
13338
13408
  }
13339
13409
  const topLevelRemoved = removedNodes.filter(
13340
- (node) => !node.parent || !removedNodes.includes(node.parent)
13410
+ (node) => !preTombstoned.has(node.id.toIDString()) && (!node.parent || !removedNodes.includes(node.parent))
13341
13411
  );
13342
- const reverseContents = topLevelRemoved.length > 0 ? topLevelRemoved.map((n) => {
13343
- const clone = n.deepcopy();
13344
- clearRemovedAt(clone);
13345
- return clone;
13346
- }) : void 0;
13412
+ const reverseContents = topLevelRemoved.length > 0 ? topLevelRemoved.map(
13413
+ (n) => cloneAndDropPreTombstoned(n, preTombstoned)
13414
+ ) : void 0;
13347
13415
  const reverseFromPos = tree.findPos(preEditFromIdx);
13348
13416
  let reverseToPos;
13349
13417
  if (insertedContentSize > 0) {
@@ -20157,6 +20225,11 @@ class Document {
20157
20225
  }
20158
20226
  ]);
20159
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
+ */
20160
20233
  clearHistory() {
20161
20234
  this.internalHistory.clearRedo();
20162
20235
  this.internalHistory.clearUndo();
@@ -20616,10 +20689,7 @@ class Document {
20616
20689
  }
20617
20690
  const ops = isUndo ? this.internalHistory.popUndo() : this.internalHistory.popRedo();
20618
20691
  if (!ops) {
20619
- throw new YorkieError(
20620
- Code.ErrRefused,
20621
- `There is no operation to be ${isUndo ? "undone" : "redone"}`
20622
- );
20692
+ return;
20623
20693
  }
20624
20694
  this.ensureClone();
20625
20695
  const ctx = ChangeContext.create(
@@ -20712,6 +20782,8 @@ class Attachment {
20712
20782
  syncMode;
20713
20783
  changeEventReceived;
20714
20784
  lastHeartbeatTime;
20785
+ pollInterval;
20786
+ pollIntervalPinned;
20715
20787
  reconnectStreamDelay;
20716
20788
  cancelled;
20717
20789
  watchStream;
@@ -20719,13 +20791,15 @@ class Attachment {
20719
20791
  watchAbortController;
20720
20792
  syncPromise;
20721
20793
  _detaching = false;
20722
- constructor(reconnectStreamDelay, resource, resourceID, syncMode) {
20794
+ constructor(reconnectStreamDelay, resource, resourceID, syncMode, pollInterval = 0, pollIntervalPinned = false) {
20723
20795
  this.reconnectStreamDelay = reconnectStreamDelay;
20724
20796
  this.resource = resource;
20725
20797
  this.resourceID = resourceID;
20726
20798
  this.syncMode = syncMode;
20727
20799
  this.changeEventReceived = syncMode !== void 0 ? false : void 0;
20728
20800
  this.lastHeartbeatTime = Date.now();
20801
+ this.pollInterval = pollInterval;
20802
+ this.pollIntervalPinned = pollIntervalPinned;
20729
20803
  this.cancelled = false;
20730
20804
  }
20731
20805
  /**
@@ -20745,6 +20819,9 @@ class Attachment {
20745
20819
  if (this.syncMode === SyncMode.RealtimePushOnly) {
20746
20820
  return this.resource.hasLocalChanges();
20747
20821
  }
20822
+ if (this.syncMode === SyncMode.Polling) {
20823
+ return Date.now() - this.lastHeartbeatTime >= this.pollInterval;
20824
+ }
20748
20825
  return this.syncMode !== SyncMode.Manual && (this.resource.hasLocalChanges() || (this.changeEventReceived ?? false));
20749
20826
  }
20750
20827
  /**
@@ -20758,7 +20835,8 @@ class Attachment {
20758
20835
  if (this.syncMode === SyncMode.Manual) {
20759
20836
  return false;
20760
20837
  }
20761
- return Date.now() - this.lastHeartbeatTime >= heartbeatInterval;
20838
+ const interval = this.pollInterval > 0 ? this.pollInterval : heartbeatInterval;
20839
+ return Date.now() - this.lastHeartbeatTime >= interval;
20762
20840
  }
20763
20841
  /**
20764
20842
  * `updateHeartbeatTime` updates the last heartbeat time.
@@ -20837,6 +20915,16 @@ class Attachment {
20837
20915
  }
20838
20916
  }
20839
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
+ }
20840
20928
  /**
20841
20929
  * `cancelWatchStream` cancels the watch stream.
20842
20930
  */
@@ -20871,7 +20959,7 @@ function createAuthInterceptor(apiKey, token) {
20871
20959
  };
20872
20960
  }
20873
20961
  const name$1 = "@yorkie-js/sdk";
20874
- const version$1 = "0.7.7";
20962
+ const version$1 = "0.7.8";
20875
20963
  const pkg$1 = {
20876
20964
  name: name$1,
20877
20965
  version: version$1
@@ -21154,8 +21242,10 @@ var SyncMode = /* @__PURE__ */ ((SyncMode2) => {
21154
21242
  SyncMode2["Realtime"] = "realtime";
21155
21243
  SyncMode2["RealtimePushOnly"] = "realtime-pushonly";
21156
21244
  SyncMode2["RealtimeSyncOff"] = "realtime-syncoff";
21245
+ SyncMode2["Polling"] = "polling";
21157
21246
  return SyncMode2;
21158
21247
  })(SyncMode || {});
21248
+ const DefaultPollingIntervalMs = 3e3;
21159
21249
  const DefaultClientOptions = {
21160
21250
  rpcAddr: "https://api.yorkie.dev",
21161
21251
  syncLoopDuration: 50,
@@ -21351,6 +21441,14 @@ class Client {
21351
21441
  doc.setActor(this.id);
21352
21442
  doc.update((_, p) => p.set(opts.initialPresence || {}));
21353
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;
21354
21452
  return this.enqueueTask(async () => {
21355
21453
  try {
21356
21454
  const res = await this.rpcClient.attachDocument(
@@ -21380,10 +21478,12 @@ class Client {
21380
21478
  this.reconnectStreamDelay,
21381
21479
  doc,
21382
21480
  res.documentId,
21383
- syncMode
21481
+ syncMode,
21482
+ pollInterval,
21483
+ pollIntervalPinned
21384
21484
  )
21385
21485
  );
21386
- if (syncMode !== "manual") {
21486
+ if (syncMode !== "manual" && syncMode !== "polling") {
21387
21487
  await this.runWatchLoop(doc.getKey());
21388
21488
  }
21389
21489
  logger.info(`[AD] c:"${this.getKey()}" attaches d:"${doc.getKey()}"`);
@@ -21399,6 +21499,7 @@ class Client {
21399
21499
  }
21400
21500
  });
21401
21501
  }
21502
+ doc.clearHistory();
21402
21503
  return doc;
21403
21504
  } catch (err) {
21404
21505
  logger.error(`[AD] c:"${this.getKey()}" err :`, err);
@@ -21509,12 +21610,23 @@ class Client {
21509
21610
  channel.setSessionID(res.sessionId);
21510
21611
  channel.updateSessionCount(Number(res.sessionCount), 0);
21511
21612
  channel.applyStatus(ChannelStatus.Attached);
21512
- 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;
21513
21623
  const attachment = new Attachment(
21514
21624
  this.reconnectStreamDelay,
21515
21625
  channel,
21516
21626
  res.sessionId,
21517
- syncMode
21627
+ syncMode,
21628
+ pollInterval,
21629
+ pollIntervalPinned
21518
21630
  );
21519
21631
  channel.subscribe("local-broadcast", (event) => {
21520
21632
  const { topic, payload, options } = event;
@@ -21590,9 +21702,17 @@ class Client {
21590
21702
  return this.enqueueTask(task);
21591
21703
  }
21592
21704
  /**
21593
- * `changeSyncMode` changes the synchronization mode of the given document.
21705
+ * `changeSyncMode` changes the synchronization mode of the given resource.
21594
21706
  */
21595
- 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) {
21596
21716
  if (!this.isActive()) {
21597
21717
  throw new YorkieError(
21598
21718
  Code.ErrClientNotActivated,
@@ -21610,19 +21730,66 @@ class Client {
21610
21730
  if (prevSyncMode === syncMode) {
21611
21731
  return doc;
21612
21732
  }
21613
- attachment.changeSyncMode(syncMode);
21614
- if (syncMode === "manual") {
21733
+ if (syncMode === "manual" || syncMode === "polling") {
21615
21734
  attachment.cancelWatchStream();
21616
- return doc;
21617
21735
  }
21736
+ attachment.changeSyncMode(syncMode);
21618
21737
  if (syncMode === "realtime") {
21619
21738
  attachment.changeEventReceived = true;
21620
21739
  }
21621
- 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();
21622
21745
  await this.runWatchLoop(doc.getKey());
21623
21746
  }
21624
21747
  return doc;
21625
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
+ }
21626
21793
  /**
21627
21794
  * `sync` implementation that handles both Document and Channel.
21628
21795
  */
@@ -22467,6 +22634,7 @@ class Client {
22467
22634
  return doc;
22468
22635
  }
22469
22636
  doc.applyChangePack(respPack);
22637
+ attachment.updateHeartbeatTime();
22470
22638
  attachment.resource.publish([
22471
22639
  {
22472
22640
  type: DocEventType.SyncStatusChanged,
@@ -22792,7 +22960,7 @@ if (typeof globalThis !== "undefined") {
22792
22960
  };
22793
22961
  }
22794
22962
  const name = "@yorkie-js/react";
22795
- const version = "0.7.7";
22963
+ const version = "0.7.8";
22796
22964
  const pkg = {
22797
22965
  name,
22798
22966
  version
@@ -23248,7 +23416,7 @@ function createChannelStore(initialState) {
23248
23416
  const ChannelContext = createContext(
23249
23417
  void 0
23250
23418
  );
23251
- function useYorkieChannel(client, clientLoading, clientError, channelKey, isRealtime, channelStore) {
23419
+ function useYorkieChannel(client, clientLoading, clientError, channelKey, syncMode, channelStore) {
23252
23420
  const channelRef = useRef(void 0);
23253
23421
  const [didMount, setDidMount] = useState(false);
23254
23422
  useEffect(() => {
@@ -23277,7 +23445,7 @@ function useYorkieChannel(client, clientLoading, clientError, channelKey, isReal
23277
23445
  }));
23278
23446
  try {
23279
23447
  const newChannel = new Channel2(channelKey);
23280
- await client.attach(newChannel, { isRealtime });
23448
+ await client.attach(newChannel, { syncMode });
23281
23449
  channelRef.current = newChannel;
23282
23450
  unsubscribe = newChannel.subscribe(() => {
23283
23451
  channelStore.setState((state) => ({
@@ -23315,12 +23483,13 @@ function useYorkieChannel(client, clientLoading, clientError, channelKey, isReal
23315
23483
  }
23316
23484
  detachChannel();
23317
23485
  };
23318
- }, [client, clientLoading, clientError, channelKey, isRealtime, didMount]);
23486
+ }, [client, clientLoading, clientError, channelKey, syncMode, didMount]);
23319
23487
  }
23320
23488
  const ChannelProvider = ({
23321
23489
  children,
23322
23490
  channelKey,
23323
- isRealtime = true
23491
+ syncMode,
23492
+ isRealtime
23324
23493
  }) => {
23325
23494
  const { client, loading: clientLoading, error: clientError } = useYorkie();
23326
23495
  const channelStoreRef = useRef(
@@ -23335,12 +23504,13 @@ const ChannelProvider = ({
23335
23504
  });
23336
23505
  }
23337
23506
  const channelStore = channelStoreRef.current;
23507
+ const resolvedSyncMode = syncMode ?? (isRealtime === false ? SyncMode.Manual : SyncMode.Realtime);
23338
23508
  useYorkieChannel(
23339
23509
  client,
23340
23510
  clientLoading,
23341
23511
  clientError,
23342
23512
  channelKey,
23343
- isRealtime,
23513
+ resolvedSyncMode,
23344
23514
  channelStore
23345
23515
  );
23346
23516
  return /* @__PURE__ */ jsx(ChannelContext.Provider, { value: channelStore, children });
@@ -23367,6 +23537,7 @@ export {
23367
23537
  ChannelProvider,
23368
23538
  Counter,
23369
23539
  DocumentProvider,
23540
+ SyncMode,
23370
23541
  Text,
23371
23542
  Tree,
23372
23543
  YorkieProvider,