cross-tab-worker-databus 0.20.71 → 0.20.86

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.
Files changed (97) hide show
  1. package/CHANGELOG.md +216 -2
  2. package/README.md +8 -0
  3. package/README.zh.md +4 -0
  4. package/dist/centrifuge-protocol.d.ts +59 -26
  5. package/dist/centrifuge-protocol.d.ts.map +1 -1
  6. package/dist/centrifuge-session.d.ts +9 -0
  7. package/dist/centrifuge-session.d.ts.map +1 -1
  8. package/dist/centrifuge.d.ts +12 -2
  9. package/dist/centrifuge.d.ts.map +1 -1
  10. package/dist/centrifuge.js +168 -88
  11. package/dist/centrifuge.js.map +3 -3
  12. package/dist/centrifuge.shared.worker.js +146 -38
  13. package/dist/centrifuge.shared.worker.js.map +3 -3
  14. package/dist/centrifuge.worker.js +140 -33
  15. package/dist/centrifuge.worker.js.map +3 -3
  16. package/dist/{chunk-D2SIT473.js → chunk-SDOV3UHG.js} +1304 -515
  17. package/dist/chunk-SDOV3UHG.js.map +7 -0
  18. package/dist/chunk-TZ7ZP7YD.js +175 -0
  19. package/dist/chunk-TZ7ZP7YD.js.map +7 -0
  20. package/dist/cjs/centrifuge.cjs +1608 -650
  21. package/dist/cjs/centrifuge.cjs.map +4 -4
  22. package/dist/cjs/hooks.cjs +37 -2
  23. package/dist/cjs/hooks.cjs.map +3 -3
  24. package/dist/cjs/index.cjs +1692 -747
  25. package/dist/cjs/index.cjs.map +4 -4
  26. package/dist/cjs/vue.cjs +45 -2
  27. package/dist/cjs/vue.cjs.map +3 -3
  28. package/dist/core/cluster.d.ts +40 -4
  29. package/dist/core/cluster.d.ts.map +1 -1
  30. package/dist/core/data-bus.d.ts +134 -77
  31. package/dist/core/data-bus.d.ts.map +1 -1
  32. package/dist/core/dedup-manager.d.ts +98 -0
  33. package/dist/core/dedup-manager.d.ts.map +1 -0
  34. package/dist/core/environment.d.ts +43 -3
  35. package/dist/core/environment.d.ts.map +1 -1
  36. package/dist/core/replay-manager.d.ts +129 -0
  37. package/dist/core/replay-manager.d.ts.map +1 -0
  38. package/dist/core/replay-persistence.d.ts +2 -1
  39. package/dist/core/replay-persistence.d.ts.map +1 -1
  40. package/dist/core/replay-pruning.d.ts +21 -0
  41. package/dist/core/replay-pruning.d.ts.map +1 -0
  42. package/dist/core/routing.d.ts +22 -3
  43. package/dist/core/routing.d.ts.map +1 -1
  44. package/dist/core/storage-batch.d.ts +0 -4
  45. package/dist/core/storage-batch.d.ts.map +1 -1
  46. package/dist/core/trace.d.ts +55 -15
  47. package/dist/core/trace.d.ts.map +1 -1
  48. package/dist/core/types.d.ts +58 -9
  49. package/dist/core/types.d.ts.map +1 -1
  50. package/dist/core/version.d.ts +2 -0
  51. package/dist/core/version.d.ts.map +1 -0
  52. package/dist/hooks.d.ts +12 -1
  53. package/dist/hooks.d.ts.map +1 -1
  54. package/dist/hooks.js +28 -2
  55. package/dist/hooks.js.map +2 -2
  56. package/dist/index.d.ts +6 -6
  57. package/dist/index.d.ts.map +1 -1
  58. package/dist/index.js +263 -166
  59. package/dist/index.js.map +3 -3
  60. package/dist/utils/constants.d.ts +170 -0
  61. package/dist/utils/constants.d.ts.map +1 -0
  62. package/dist/utils/error-utils.d.ts +28 -0
  63. package/dist/utils/error-utils.d.ts.map +1 -0
  64. package/dist/utils/metadata.d.ts +16 -0
  65. package/dist/utils/metadata.d.ts.map +1 -0
  66. package/dist/utils/storage-utils.d.ts +24 -0
  67. package/dist/utils/storage-utils.d.ts.map +1 -0
  68. package/dist/utils/validation.d.ts +76 -0
  69. package/dist/utils/validation.d.ts.map +1 -0
  70. package/dist/vue.d.ts +13 -1
  71. package/dist/vue.d.ts.map +1 -1
  72. package/dist/vue.js +36 -2
  73. package/dist/vue.js.map +2 -2
  74. package/dist/websocket.d.ts +6 -1
  75. package/dist/websocket.d.ts.map +1 -1
  76. package/dist/worker-mode.d.ts +6 -2
  77. package/dist/worker-mode.d.ts.map +1 -1
  78. package/docs/README.md +1 -0
  79. package/docs/api.md +135 -4
  80. package/docs/architecture.md +92 -1
  81. package/docs/benchmarks.md +24 -0
  82. package/docs/capabilities.md +24 -3
  83. package/docs/configuration.md +58 -0
  84. package/docs/getting-started.md +35 -0
  85. package/docs/release-checklist.md +28 -5
  86. package/docs/roadmap.md +81 -5
  87. package/docs/zh/README.md +2 -1
  88. package/docs/zh/api.md +134 -4
  89. package/docs/zh/architecture.md +61 -1
  90. package/docs/zh/benchmarks.md +24 -0
  91. package/docs/zh/capabilities.md +21 -3
  92. package/docs/zh/configuration.md +53 -0
  93. package/docs/zh/getting-started.md +35 -0
  94. package/docs/zh/release-checklist.md +29 -6
  95. package/docs/zh/roadmap.md +93 -5
  96. package/package.json +39 -16
  97. package/dist/chunk-D2SIT473.js.map +0 -7
package/dist/index.js CHANGED
@@ -2,33 +2,44 @@ import {
2
2
  CrossTabDataBus,
3
3
  DEFAULT_MAX_ACTIVE_WORKERS,
4
4
  WorkerClusterRuntime,
5
+ approximatePayloadBytes,
6
+ assertPositiveFiniteNumber,
7
+ assertPositiveSafeInteger,
8
+ assertPruneStrategy,
5
9
  createBrowserEnvironment,
6
10
  createOpaqueKey,
11
+ createStorageEventChannel,
12
+ effectiveWorkerLoad,
7
13
  getOrCreateTabId,
8
14
  hasActiveOwner,
9
15
  isWildcardTopic,
10
16
  parseDataBusPublication,
17
+ pruneReplayHistory,
11
18
  selectActiveWorkers,
12
19
  selectLeastLoadedWorker,
13
20
  selectRebalanceTarget,
14
21
  selectWorkerBackend,
15
22
  topicMatchesPattern
16
- } from "./chunk-D2SIT473.js";
23
+ } from "./chunk-SDOV3UHG.js";
24
+ import {
25
+ DEFAULT_STORAGE_PREFIX,
26
+ PRUNE_STRATEGY,
27
+ WORKER_STATUS,
28
+ WS_OP
29
+ } from "./chunk-TZ7ZP7YD.js";
17
30
 
18
31
  // src/core/replay-persistence.ts
19
32
  function createIndexedDbReplayPersistence(options) {
20
33
  const indexedDb = globalThis.indexedDB;
21
34
  if (!indexedDb) throw new Error("IndexedDB is unavailable in this environment.");
22
- const dbName = options.dbName ?? "cross-tab-worker-databus";
35
+ const dbName = options.dbName ?? DEFAULT_STORAGE_PREFIX;
23
36
  const storeName = "replay";
24
37
  const maxPerTopic = options.maxPerTopic;
25
- const pruneStrategy = options.pruneStrategy ?? "count";
38
+ const pruneStrategy = options.pruneStrategy ?? PRUNE_STRATEGY.COUNT;
26
39
  const retentionMs = options.retentionMs;
27
- if (!["count", "age", "both"].includes(pruneStrategy)) throw new TypeError("pruneStrategy must be count, age, or both.");
28
- if (retentionMs !== void 0 && (!Number.isFinite(retentionMs) || retentionMs <= 0)) throw new TypeError("retentionMs must be a positive finite number.");
29
- if (!Number.isSafeInteger(maxPerTopic) || maxPerTopic <= 0) {
30
- throw new TypeError(`maxPerTopic must be a positive safe integer, got ${String(maxPerTopic)}.`);
31
- }
40
+ assertPruneStrategy(pruneStrategy);
41
+ if (retentionMs !== void 0) assertPositiveFiniteNumber(retentionMs, "retentionMs");
42
+ assertPositiveSafeInteger(maxPerTopic, "maxPerTopic");
32
43
  let dbPromise = null;
33
44
  const invalidate = (db) => {
34
45
  if (dbPromise) {
@@ -40,15 +51,94 @@ function createIndexedDbReplayPersistence(options) {
40
51
  }, () => void 0);
41
52
  }
42
53
  };
43
- let mutationQueue = Promise.resolve();
44
- const serializeMutation = (mutation) => {
45
- const next = mutationQueue.then(mutation, mutation);
46
- mutationQueue = next.catch(() => void 0);
47
- return next;
54
+ const pending = [];
55
+ let draining = false;
56
+ const drain = async () => {
57
+ if (draining) return;
58
+ draining = true;
59
+ try {
60
+ await Promise.resolve();
61
+ while (pending.length > 0) {
62
+ const head = pending[0].mutation;
63
+ if (head.kind === "batch") {
64
+ const merged = [];
65
+ const entries = [];
66
+ while (pending.length > 0) {
67
+ const next = pending[0].mutation;
68
+ if (next.kind !== "batch") break;
69
+ merged.push(...next.messages);
70
+ entries.push({ resolve: pending[0].resolve, reject: pending[0].reject });
71
+ pending.shift();
72
+ }
73
+ try {
74
+ await appendTransaction(merged);
75
+ for (const entry of entries) entry.resolve();
76
+ } catch (error) {
77
+ for (const entry of entries) entry.reject(error);
78
+ }
79
+ } else {
80
+ const entry = pending.shift();
81
+ try {
82
+ await head.run();
83
+ entry.resolve();
84
+ } catch (error) {
85
+ entry.reject(error);
86
+ }
87
+ }
88
+ }
89
+ } finally {
90
+ draining = false;
91
+ }
48
92
  };
93
+ const enqueue = (mutation) => new Promise((resolve, reject) => {
94
+ pending.push({ mutation, resolve, reject });
95
+ void drain();
96
+ });
97
+ const appendTransaction = (messages) => (async () => {
98
+ const db = await open();
99
+ return new Promise((resolve, reject) => {
100
+ let transaction;
101
+ try {
102
+ transaction = db.transaction(storeName, "readwrite");
103
+ } catch (error) {
104
+ invalidate(db);
105
+ reject(error);
106
+ return;
107
+ }
108
+ const store = transaction.objectStore(storeName);
109
+ const grouped = /* @__PURE__ */ new Map();
110
+ for (const message of messages) {
111
+ grouped.set(message.topic, [...grouped.get(message.topic) ?? [], message]);
112
+ }
113
+ let hasError = false;
114
+ const fail = (error) => {
115
+ if (hasError) return;
116
+ hasError = true;
117
+ invalidate(db);
118
+ reject(error);
119
+ };
120
+ for (const [topic, topicMessages] of grouped) {
121
+ const request = store.get(topic);
122
+ request.onsuccess = () => {
123
+ if (hasError) return;
124
+ const history = pruneReplayHistory(
125
+ (request.result?.messages ?? []).concat(topicMessages),
126
+ { maxPerTopic, pruneStrategy, retentionMs, now: Date.now() }
127
+ );
128
+ store.put({ topic, messages: history });
129
+ };
130
+ request.onerror = () => fail(request.error ?? new Error("Failed to read replay history."));
131
+ }
132
+ transaction.oncomplete = () => {
133
+ if (!hasError) resolve();
134
+ };
135
+ transaction.onerror = () => fail(transaction.error ?? new Error("Failed to persist replay history."));
136
+ transaction.onabort = () => fail(transaction.error ?? new Error("Failed to persist replay history."));
137
+ });
138
+ })();
49
139
  const open = () => {
50
140
  if (dbPromise) return dbPromise;
51
- const pending = new Promise((resolve, reject) => {
141
+ const pending2 = new Promise((resolve, reject) => {
52
142
  const request = indexedDb.open(dbName, 1);
53
143
  request.onupgradeneeded = () => request.result.createObjectStore(storeName, { keyPath: "topic" });
54
144
  request.onsuccess = () => {
@@ -61,179 +151,156 @@ function createIndexedDbReplayPersistence(options) {
61
151
  };
62
152
  request.onerror = () => reject(request.error ?? new Error("Failed to open replay database."));
63
153
  });
64
- dbPromise = pending;
65
- void pending.catch(() => {
66
- if (dbPromise === pending) dbPromise = null;
154
+ dbPromise = pending2;
155
+ void pending2.catch(() => {
156
+ if (dbPromise === pending2) dbPromise = null;
67
157
  });
68
- return pending;
158
+ return pending2;
69
159
  };
70
160
  return {
71
161
  async load() {
72
162
  const db = await open();
73
163
  return new Promise((resolve, reject) => {
164
+ let transaction;
74
165
  let request;
75
166
  try {
76
- request = db.transaction(storeName, "readonly").objectStore(storeName).getAll();
167
+ transaction = db.transaction(storeName, "readonly");
168
+ request = transaction.objectStore(storeName).getAll();
77
169
  } catch (error) {
78
170
  invalidate(db);
79
171
  reject(error);
80
172
  return;
81
173
  }
82
- request.onsuccess = () => resolve(request.result.flatMap((record) => record.messages));
83
- request.onerror = () => {
174
+ let settled = false;
175
+ const fail = (error) => {
176
+ if (settled) return;
177
+ settled = true;
84
178
  invalidate(db);
85
- reject(request.error ?? new Error("Failed to load replay history."));
179
+ reject(error);
86
180
  };
181
+ let records = [];
182
+ request.onsuccess = () => {
183
+ records = request.result;
184
+ };
185
+ request.onerror = () => fail(request.error ?? new Error("Failed to load replay history."));
186
+ transaction.oncomplete = () => {
187
+ if (settled) return;
188
+ settled = true;
189
+ resolve(records.flatMap((record) => record.messages));
190
+ };
191
+ transaction.onabort = () => fail(transaction.error ?? new Error("Failed to load replay history."));
87
192
  });
88
193
  },
89
194
  append(message) {
90
- return serializeMutation(async () => {
91
- const db = await open();
92
- await new Promise((resolve, reject) => {
93
- let transaction;
94
- try {
95
- transaction = db.transaction(storeName, "readwrite");
96
- } catch (error) {
97
- invalidate(db);
98
- reject(error);
99
- return;
100
- }
101
- const store = transaction.objectStore(storeName);
102
- const request = store.get(message.topic);
103
- request.onsuccess = () => {
104
- let messages = (request.result?.messages ?? []).concat(message);
105
- if (pruneStrategy !== "count" && retentionMs !== void 0) {
106
- const cutoff = Date.now() - retentionMs;
107
- messages = messages.filter((item) => item.timestamp === void 0 || item.timestamp >= cutoff);
108
- }
109
- if (pruneStrategy !== "age") messages = messages.slice(-maxPerTopic);
110
- store.put({ topic: message.topic, messages });
111
- };
112
- request.onerror = () => {
113
- invalidate(db);
114
- reject(request.error ?? new Error("Failed to read replay history."));
115
- };
116
- transaction.oncomplete = () => resolve();
117
- transaction.onerror = () => {
118
- invalidate(db);
119
- reject(transaction.error ?? new Error("Failed to persist replay history."));
120
- };
121
- });
122
- });
195
+ return enqueue({ kind: "batch", messages: [message] });
123
196
  },
124
197
  appendBatch(messages) {
125
198
  if (messages.length === 0) return Promise.resolve();
126
- return serializeMutation(async () => {
127
- const db = await open();
128
- await new Promise((resolve, reject) => {
129
- let transaction;
130
- try {
131
- transaction = db.transaction(storeName, "readwrite");
132
- } catch (error) {
133
- invalidate(db);
134
- reject(error);
135
- return;
136
- }
137
- const store = transaction.objectStore(storeName);
138
- const grouped = /* @__PURE__ */ new Map();
139
- for (const message of messages) grouped.set(message.topic, [...grouped.get(message.topic) ?? [], message]);
140
- for (const [topic, topicMessages] of grouped) {
141
- const request = store.get(topic);
142
- request.onsuccess = () => {
143
- let history = (request.result?.messages ?? []).concat(topicMessages);
144
- if (pruneStrategy !== "count" && retentionMs !== void 0) {
145
- const cutoff = Date.now() - retentionMs;
146
- history = history.filter((item) => item.timestamp === void 0 || item.timestamp >= cutoff);
147
- }
148
- if (pruneStrategy !== "age") history = history.slice(-maxPerTopic);
149
- store.put({ topic, messages: history });
150
- };
151
- request.onerror = () => {
152
- invalidate(db);
153
- reject(request.error ?? new Error("Failed to read replay history."));
154
- };
155
- }
156
- transaction.oncomplete = () => resolve();
157
- transaction.onerror = () => {
158
- invalidate(db);
159
- reject(transaction.error ?? new Error("Failed to persist replay history batch."));
160
- };
161
- });
162
- });
199
+ return enqueue({ kind: "batch", messages });
163
200
  },
164
201
  clear() {
165
- return serializeMutation(async () => {
166
- const db = await open();
167
- await new Promise((resolve, reject) => {
168
- let transaction;
169
- try {
170
- transaction = db.transaction(storeName, "readwrite");
171
- } catch (error) {
172
- invalidate(db);
173
- reject(error);
174
- return;
175
- }
176
- transaction.objectStore(storeName).clear();
177
- transaction.oncomplete = () => resolve();
178
- transaction.onerror = () => {
179
- invalidate(db);
180
- reject(transaction.error ?? new Error("Failed to clear replay history."));
181
- };
182
- });
202
+ return enqueue({
203
+ kind: "run",
204
+ run: () => (async () => {
205
+ const db = await open();
206
+ return new Promise((resolve, reject) => {
207
+ let transaction;
208
+ try {
209
+ transaction = db.transaction(storeName, "readwrite");
210
+ } catch (error) {
211
+ invalidate(db);
212
+ reject(error);
213
+ return;
214
+ }
215
+ let settled = false;
216
+ const fail = (error) => {
217
+ if (settled) return;
218
+ settled = true;
219
+ invalidate(db);
220
+ reject(error);
221
+ };
222
+ transaction.objectStore(storeName).clear();
223
+ transaction.oncomplete = () => {
224
+ settled = true;
225
+ resolve();
226
+ };
227
+ transaction.onerror = () => fail(transaction.error ?? new Error("Failed to clear replay history."));
228
+ transaction.onabort = () => fail(transaction.error ?? new Error("Failed to clear replay history."));
229
+ });
230
+ })()
183
231
  });
184
232
  },
185
233
  clearTopic(topic) {
186
- return serializeMutation(async () => {
187
- const db = await open();
188
- await new Promise((resolve, reject) => {
189
- let transaction;
190
- try {
191
- transaction = db.transaction(storeName, "readwrite");
192
- } catch (error) {
193
- invalidate(db);
194
- reject(error);
195
- return;
196
- }
197
- transaction.objectStore(storeName).delete(topic);
198
- transaction.oncomplete = () => resolve();
199
- transaction.onerror = () => {
200
- invalidate(db);
201
- reject(transaction.error ?? new Error("Failed to clear topic replay history."));
202
- };
203
- });
234
+ return enqueue({
235
+ kind: "run",
236
+ run: () => (async () => {
237
+ const db = await open();
238
+ return new Promise((resolve, reject) => {
239
+ let transaction;
240
+ try {
241
+ transaction = db.transaction(storeName, "readwrite");
242
+ } catch (error) {
243
+ invalidate(db);
244
+ reject(error);
245
+ return;
246
+ }
247
+ let settled = false;
248
+ const fail = (error) => {
249
+ if (settled) return;
250
+ settled = true;
251
+ invalidate(db);
252
+ reject(error);
253
+ };
254
+ transaction.objectStore(storeName).delete(topic);
255
+ transaction.oncomplete = () => {
256
+ settled = true;
257
+ resolve();
258
+ };
259
+ transaction.onerror = () => fail(transaction.error ?? new Error("Failed to clear topic replay history."));
260
+ transaction.onabort = () => fail(transaction.error ?? new Error("Failed to clear topic replay history."));
261
+ });
262
+ })()
204
263
  });
205
264
  },
206
265
  clearBefore(timestamp) {
207
- return serializeMutation(async () => {
208
- const db = await open();
209
- await new Promise((resolve, reject) => {
210
- let transaction;
211
- try {
212
- transaction = db.transaction(storeName, "readwrite");
213
- } catch (error) {
214
- invalidate(db);
215
- reject(error);
216
- return;
217
- }
218
- const store = transaction.objectStore(storeName);
219
- const request = store.getAll();
220
- request.onsuccess = () => {
221
- for (const record of request.result) {
222
- const messages = record.messages.filter((message) => message.timestamp === void 0 || message.timestamp >= timestamp);
223
- if (messages.length === 0) store.delete(record.topic);
224
- else if (messages.length !== record.messages.length) store.put({ topic: record.topic, messages });
266
+ return enqueue({
267
+ kind: "run",
268
+ run: () => (async () => {
269
+ const db = await open();
270
+ return new Promise((resolve, reject) => {
271
+ let transaction;
272
+ try {
273
+ transaction = db.transaction(storeName, "readwrite");
274
+ } catch (error) {
275
+ invalidate(db);
276
+ reject(error);
277
+ return;
225
278
  }
226
- };
227
- request.onerror = () => {
228
- invalidate(db);
229
- reject(request.error ?? new Error("Failed to read replay history."));
230
- };
231
- transaction.oncomplete = () => resolve();
232
- transaction.onerror = () => {
233
- invalidate(db);
234
- reject(transaction.error ?? new Error("Failed to prune replay history."));
235
- };
236
- });
279
+ let settled = false;
280
+ const fail = (error) => {
281
+ if (settled) return;
282
+ settled = true;
283
+ invalidate(db);
284
+ reject(error);
285
+ };
286
+ const store = transaction.objectStore(storeName);
287
+ const request = store.getAll();
288
+ request.onsuccess = () => {
289
+ for (const record of request.result) {
290
+ const messages = record.messages.filter((message) => message.timestamp === void 0 || message.timestamp >= timestamp);
291
+ if (messages.length === 0) store.delete(record.topic);
292
+ else if (messages.length !== record.messages.length) store.put({ topic: record.topic, messages });
293
+ }
294
+ };
295
+ request.onerror = () => fail(request.error ?? new Error("Failed to read replay history."));
296
+ transaction.oncomplete = () => {
297
+ settled = true;
298
+ resolve();
299
+ };
300
+ transaction.onerror = () => fail(transaction.error ?? new Error("Failed to prune replay history."));
301
+ transaction.onabort = () => fail(transaction.error ?? new Error("Failed to prune replay history."));
302
+ });
303
+ })()
237
304
  });
238
305
  }
239
306
  };
@@ -245,6 +312,7 @@ var WebSocketTransport = class {
245
312
  constructor(connection) {
246
313
  this.connection = connection;
247
314
  }
315
+ connection;
248
316
  diagnosticsName = "websocket";
249
317
  diagnosticsBackend = "native-websocket";
250
318
  socket = null;
@@ -261,22 +329,22 @@ var WebSocketTransport = class {
261
329
  try {
262
330
  socket = factory(config.url, protocols);
263
331
  } catch (error) {
264
- handlers.onStatus("error");
332
+ handlers.onStatus(WORKER_STATUS.ERROR);
265
333
  handlers.onError(error);
266
334
  return;
267
335
  }
268
336
  socket.onopen = () => {
269
337
  if (this.socket !== socket || this.handlers !== handlers) return;
270
338
  for (const topic of this.subscribedTopics) {
271
- this.sendFrame({ op: "subscribe", topic });
339
+ this.sendFrame({ op: WS_OP.SUBSCRIBE, topic });
272
340
  }
273
- handlers.onStatus("connected");
341
+ handlers.onStatus(WORKER_STATUS.CONNECTED);
274
342
  };
275
343
  socket.onclose = () => {
276
- if (this.socket === socket && this.handlers === handlers) handlers.onStatus("disconnected");
344
+ if (this.socket === socket && this.handlers === handlers) handlers.onStatus(WORKER_STATUS.DISCONNECTED);
277
345
  };
278
346
  socket.onerror = () => {
279
- if (this.socket === socket && this.handlers === handlers) handlers.onStatus("error");
347
+ if (this.socket === socket && this.handlers === handlers) handlers.onStatus(WORKER_STATUS.ERROR);
280
348
  };
281
349
  socket.onmessage = (event) => {
282
350
  if (this.socket === socket && this.handlers === handlers) void this.handleMessage(event.data);
@@ -287,12 +355,12 @@ var WebSocketTransport = class {
287
355
  * not duplicate the local tracking entry. */
288
356
  subscribe(topic) {
289
357
  this.subscribedTopics.add(topic);
290
- this.sendFrame({ op: "subscribe", topic });
358
+ this.sendFrame({ op: WS_OP.SUBSCRIBE, topic });
291
359
  }
292
360
  /** Idempotent: unsubscribing an unknown topic is a no-op. */
293
361
  unsubscribe(topic) {
294
362
  this.subscribedTopics.delete(topic);
295
- this.sendFrame({ op: "unsubscribe", topic });
363
+ this.sendFrame({ op: WS_OP.UNSUBSCRIBE, topic });
296
364
  }
297
365
  /** Publish `data` to `topic` as a JSON frame. Requires an open socket. */
298
366
  publish(topic, data, options) {
@@ -301,13 +369,39 @@ var WebSocketTransport = class {
301
369
  return;
302
370
  }
303
371
  this.sendFrame({
304
- op: "publish",
372
+ op: WS_OP.PUBLISH,
305
373
  topic,
306
374
  data,
307
375
  ...options?.messageId === void 0 ? {} : { messageId: options.messageId },
308
376
  ...options?.timestamp === void 0 ? {} : { timestamp: options.timestamp }
309
377
  });
310
378
  }
379
+ /** Publish many items for one topic as a single wire frame. One-item
380
+ * batches delegate to `publish` so the legacy single-publication frame
381
+ * shape (including binary framing) is preserved. */
382
+ publishBatch(topic, items) {
383
+ if (items.length === 0) return;
384
+ if (items.length === 1) {
385
+ const single = items[0];
386
+ return this.publish(topic, single.data, {
387
+ ...single.messageId === void 0 ? {} : { messageId: single.messageId },
388
+ ...single.timestamp === void 0 ? {} : { timestamp: single.timestamp }
389
+ });
390
+ }
391
+ if (this.socket?.readyState !== WS_OPEN) {
392
+ this.handlers?.onError(new Error('WebSocket is not open; dropped "publishBatch" frame.'));
393
+ return;
394
+ }
395
+ this.socket.send(JSON.stringify({
396
+ op: WS_OP.PUBLISH_BATCH,
397
+ topic,
398
+ items: items.map((item) => ({
399
+ data: item.data instanceof ArrayBuffer ? Array.from(new Uint8Array(item.data)) : item.data,
400
+ ...item.messageId === void 0 ? {} : { messageId: item.messageId },
401
+ ...item.timestamp === void 0 ? {} : { timestamp: item.timestamp }
402
+ }))
403
+ }));
404
+ }
311
405
  /** Close the socket and drop all state. Safe to call multiple times. */
312
406
  stop() {
313
407
  const socket = this.socket;
@@ -329,7 +423,7 @@ var WebSocketTransport = class {
329
423
  sendBinaryFrame(topic, data, messageId, timestamp) {
330
424
  if (messageId !== void 0 || timestamp !== void 0) {
331
425
  this.sendFrame({
332
- op: "publish",
426
+ op: WS_OP.PUBLISH,
333
427
  topic,
334
428
  data: Array.from(new Uint8Array(data)),
335
429
  ...messageId === void 0 ? {} : { messageId },
@@ -409,10 +503,13 @@ export {
409
503
  DEFAULT_MAX_ACTIVE_WORKERS,
410
504
  WebSocketTransport,
411
505
  WorkerClusterRuntime,
506
+ approximatePayloadBytes,
412
507
  createBrowserEnvironment,
413
508
  createIndexedDbReplayPersistence,
414
509
  createOpaqueKey,
510
+ createStorageEventChannel,
415
511
  createWebSocketDataBus,
512
+ effectiveWorkerLoad,
416
513
  getOrCreateTabId,
417
514
  hasActiveOwner,
418
515
  isWildcardTopic,