jazz-tools 0.19.21 → 0.19.22

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.
@@ -20,21 +20,26 @@ async function startWorker(options) {
20
20
  } = options;
21
21
  let node = void 0;
22
22
  const peers = [];
23
- const wsPeer = new WebSocketPeerWithReconnection({
24
- peer: syncServer,
25
- reconnectionTimeout: 100,
26
- addPeer: (peer) => {
27
- if (node) {
28
- node.syncManager.addPeer(peer);
29
- } else {
30
- peers.push(peer);
31
- }
32
- },
33
- removePeer: () => {
34
- },
35
- WebSocketConstructor: options.WebSocket
36
- });
37
- wsPeer.enable();
23
+ let wsPeer;
24
+ if (options.peer) {
25
+ peers.push(options.peer);
26
+ } else {
27
+ wsPeer = new WebSocketPeerWithReconnection({
28
+ peer: syncServer,
29
+ reconnectionTimeout: 100,
30
+ addPeer: (peer) => {
31
+ if (node) {
32
+ node.syncManager.addPeer(peer);
33
+ } else {
34
+ peers.push(peer);
35
+ }
36
+ },
37
+ removePeer: () => {
38
+ },
39
+ WebSocketConstructor: options.WebSocket
40
+ });
41
+ wsPeer.enable();
42
+ }
38
43
  if (!accountID) {
39
44
  throw new Error("No accountID provided");
40
45
  }
@@ -67,7 +72,7 @@ async function startWorker(options) {
67
72
  const inbox = skipInboxLoad ? void 0 : await Inbox.load(account);
68
73
  async function done() {
69
74
  await context.account.$jazz.waitForAllCoValuesSync();
70
- wsPeer.disable();
75
+ wsPeer?.disable();
71
76
  context.done();
72
77
  }
73
78
  const inboxPublicApi = inbox ? {
@@ -93,11 +98,17 @@ async function startWorker(options) {
93
98
  * Wait for the connection to the sync server to be established.
94
99
  *
95
100
  * If already connected, it will resolve immediately.
101
+ * Returns immediately if using a custom peer.
96
102
  */
97
103
  waitForConnection() {
98
- return wsPeer.waitUntilConnected();
104
+ return wsPeer?.waitUntilConnected() ?? Promise.resolve();
99
105
  },
100
106
  subscribeToConnectionChange(listener) {
107
+ if (!wsPeer) {
108
+ listener(true);
109
+ return () => {
110
+ };
111
+ }
101
112
  wsPeer.subscribe(listener);
102
113
  return () => {
103
114
  wsPeer.unsubscribe(listener);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/worker/index.ts"],"sourcesContent":["import {\n AgentSecret,\n CryptoProvider,\n LocalNode,\n Peer,\n StorageAPI,\n} from \"cojson\";\nimport {\n type AnyWebSocketConstructor,\n WebSocketPeerWithReconnection,\n} from \"cojson-transport-ws\";\nimport { WasmCrypto } from \"cojson/crypto/WasmCrypto\";\nimport {\n Account,\n AccountClass,\n AnyAccountSchema,\n CoValueFromRaw,\n Inbox,\n InstanceOfSchema,\n Loaded,\n createJazzContextFromExistingCredentials,\n MockSessionProvider,\n} from \"jazz-tools\";\n\ntype WorkerOptions<\n S extends\n | (AccountClass<Account> & CoValueFromRaw<Account>)\n | AnyAccountSchema,\n> = {\n accountID?: string;\n accountSecret?: string;\n syncServer?: string;\n WebSocket?: AnyWebSocketConstructor;\n AccountSchema?: S;\n crypto?: CryptoProvider;\n /**\n * If true, the inbox will not be loaded.\n */\n skipInboxLoad?: boolean;\n /**\n * If false, the worker will not set in the global account context\n */\n asActiveAccount?: boolean;\n storage?: StorageAPI;\n};\n\n/** @category Context Creation */\nexport async function startWorker<\n S extends\n | (AccountClass<Account> & CoValueFromRaw<Account>)\n | AnyAccountSchema,\n>(options: WorkerOptions<S>) {\n const {\n accountID = process.env.JAZZ_WORKER_ACCOUNT,\n accountSecret = process.env.JAZZ_WORKER_SECRET,\n syncServer = \"wss://cloud.jazz.tools\",\n AccountSchema = Account as unknown as S,\n skipInboxLoad = false,\n asActiveAccount = true,\n } = options;\n\n let node: LocalNode | undefined = undefined;\n\n const peers: Peer[] = [];\n\n const wsPeer = new WebSocketPeerWithReconnection({\n peer: syncServer,\n reconnectionTimeout: 100,\n addPeer: (peer) => {\n if (node) {\n node.syncManager.addPeer(peer);\n } else {\n peers.push(peer);\n }\n },\n removePeer: () => {},\n WebSocketConstructor: options.WebSocket,\n });\n\n wsPeer.enable();\n\n if (!accountID) {\n throw new Error(\"No accountID provided\");\n }\n if (!accountSecret) {\n throw new Error(\"No accountSecret provided\");\n }\n if (!accountID.startsWith(\"co_\")) {\n throw new Error(\"Invalid accountID\");\n }\n if (!accountSecret?.startsWith(\"sealerSecret_\")) {\n throw new Error(\"Invalid accountSecret\");\n }\n\n const context = await createJazzContextFromExistingCredentials({\n credentials: {\n accountID: accountID,\n secret: accountSecret as AgentSecret,\n },\n AccountSchema,\n sessionProvider: new MockSessionProvider(),\n peers,\n crypto: options.crypto ?? (await WasmCrypto.create()),\n asActiveAccount,\n storage: options.storage,\n });\n\n const account = context.account as InstanceOfSchema<S>;\n node = account.$jazz.localNode;\n\n if (!account.$jazz.refs.profile?.id) {\n throw new Error(\"Account has no profile\");\n }\n\n const inbox = skipInboxLoad ? undefined : await Inbox.load(account);\n\n async function done() {\n await context.account.$jazz.waitForAllCoValuesSync();\n\n wsPeer.disable();\n context.done();\n }\n\n const inboxPublicApi = inbox\n ? {\n subscribe: inbox.subscribe.bind(inbox) as Inbox[\"subscribe\"],\n }\n : {\n subscribe: () => {},\n };\n\n return {\n /**\n * The worker account instance.\n */\n worker: context.account as Loaded<S>,\n experimental: {\n /**\n * API to subscribe to the inbox messages.\n *\n * More info on the Inbox API: https://jazz.tools/docs/react/server-side/inbox\n */\n inbox: inboxPublicApi,\n },\n /**\n * Wait for the connection to the sync server to be established.\n *\n * If already connected, it will resolve immediately.\n */\n waitForConnection() {\n return wsPeer.waitUntilConnected();\n },\n subscribeToConnectionChange(listener: (connected: boolean) => void) {\n wsPeer.subscribe(listener);\n\n return () => {\n wsPeer.unsubscribe(listener);\n };\n },\n /**\n * Waits for all CoValues to sync and then shuts down the worker.\n *\n * To only wait for sync use worker.$jazz.waitForAllCoValuesSync()\n *\n * @deprecated Use shutdownWorker\n */\n done,\n /**\n * Waits for all CoValues to sync and then shuts down the worker.\n *\n * To only wait for sync use worker.$jazz.waitForAllCoValuesSync()\n */\n shutdownWorker() {\n return done();\n },\n };\n}\n"],"mappings":";AAOA;AAAA,EAEE;AAAA,OACK;AACP,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EAIA;AAAA,EAGA;AAAA,EACA;AAAA,OACK;AAyBP,eAAsB,YAIpB,SAA2B;AAC3B,QAAM;AAAA,IACJ,YAAY,QAAQ,IAAI;AAAA,IACxB,gBAAgB,QAAQ,IAAI;AAAA,IAC5B,aAAa;AAAA,IACb,gBAAgB;AAAA,IAChB,gBAAgB;AAAA,IAChB,kBAAkB;AAAA,EACpB,IAAI;AAEJ,MAAI,OAA8B;AAElC,QAAM,QAAgB,CAAC;AAEvB,QAAM,SAAS,IAAI,8BAA8B;AAAA,IAC/C,MAAM;AAAA,IACN,qBAAqB;AAAA,IACrB,SAAS,CAAC,SAAS;AACjB,UAAI,MAAM;AACR,aAAK,YAAY,QAAQ,IAAI;AAAA,MAC/B,OAAO;AACL,cAAM,KAAK,IAAI;AAAA,MACjB;AAAA,IACF;AAAA,IACA,YAAY,MAAM;AAAA,IAAC;AAAA,IACnB,sBAAsB,QAAQ;AAAA,EAChC,CAAC;AAED,SAAO,OAAO;AAEd,MAAI,CAAC,WAAW;AACd,UAAM,IAAI,MAAM,uBAAuB;AAAA,EACzC;AACA,MAAI,CAAC,eAAe;AAClB,UAAM,IAAI,MAAM,2BAA2B;AAAA,EAC7C;AACA,MAAI,CAAC,UAAU,WAAW,KAAK,GAAG;AAChC,UAAM,IAAI,MAAM,mBAAmB;AAAA,EACrC;AACA,MAAI,CAAC,eAAe,WAAW,eAAe,GAAG;AAC/C,UAAM,IAAI,MAAM,uBAAuB;AAAA,EACzC;AAEA,QAAM,UAAU,MAAM,yCAAyC;AAAA,IAC7D,aAAa;AAAA,MACX;AAAA,MACA,QAAQ;AAAA,IACV;AAAA,IACA;AAAA,IACA,iBAAiB,IAAI,oBAAoB;AAAA,IACzC;AAAA,IACA,QAAQ,QAAQ,UAAW,MAAM,WAAW,OAAO;AAAA,IACnD;AAAA,IACA,SAAS,QAAQ;AAAA,EACnB,CAAC;AAED,QAAM,UAAU,QAAQ;AACxB,SAAO,QAAQ,MAAM;AAErB,MAAI,CAAC,QAAQ,MAAM,KAAK,SAAS,IAAI;AACnC,UAAM,IAAI,MAAM,wBAAwB;AAAA,EAC1C;AAEA,QAAM,QAAQ,gBAAgB,SAAY,MAAM,MAAM,KAAK,OAAO;AAElE,iBAAe,OAAO;AACpB,UAAM,QAAQ,QAAQ,MAAM,uBAAuB;AAEnD,WAAO,QAAQ;AACf,YAAQ,KAAK;AAAA,EACf;AAEA,QAAM,iBAAiB,QACnB;AAAA,IACE,WAAW,MAAM,UAAU,KAAK,KAAK;AAAA,EACvC,IACA;AAAA,IACE,WAAW,MAAM;AAAA,IAAC;AAAA,EACpB;AAEJ,SAAO;AAAA;AAAA;AAAA;AAAA,IAIL,QAAQ,QAAQ;AAAA,IAChB,cAAc;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAMZ,OAAO;AAAA,IACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,oBAAoB;AAClB,aAAO,OAAO,mBAAmB;AAAA,IACnC;AAAA,IACA,4BAA4B,UAAwC;AAClE,aAAO,UAAU,QAAQ;AAEzB,aAAO,MAAM;AACX,eAAO,YAAY,QAAQ;AAAA,MAC7B;AAAA,IACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,iBAAiB;AACf,aAAO,KAAK;AAAA,IACd;AAAA,EACF;AACF;","names":[]}
1
+ {"version":3,"sources":["../../src/worker/index.ts"],"sourcesContent":["import {\n AgentSecret,\n CryptoProvider,\n LocalNode,\n Peer,\n StorageAPI,\n} from \"cojson\";\nimport {\n type AnyWebSocketConstructor,\n WebSocketPeerWithReconnection,\n} from \"cojson-transport-ws\";\nimport { WasmCrypto } from \"cojson/crypto/WasmCrypto\";\nimport {\n Account,\n AccountClass,\n AnyAccountSchema,\n CoValueFromRaw,\n Inbox,\n InstanceOfSchema,\n Loaded,\n createJazzContextFromExistingCredentials,\n MockSessionProvider,\n} from \"jazz-tools\";\n\ntype WorkerOptions<\n S extends\n | (AccountClass<Account> & CoValueFromRaw<Account>)\n | AnyAccountSchema,\n> = {\n accountID?: string;\n accountSecret?: string;\n /**\n * A peer to connect to for synchronization.\n * If provided, syncServer is ignored.\n */\n peer?: Peer;\n syncServer?: string;\n WebSocket?: AnyWebSocketConstructor;\n AccountSchema?: S;\n crypto?: CryptoProvider;\n /**\n * If true, the inbox will not be loaded.\n */\n skipInboxLoad?: boolean;\n /**\n * If false, the worker will not set in the global account context\n */\n asActiveAccount?: boolean;\n storage?: StorageAPI;\n};\n\n/** @category Context Creation */\nexport async function startWorker<\n S extends\n | (AccountClass<Account> & CoValueFromRaw<Account>)\n | AnyAccountSchema,\n>(options: WorkerOptions<S>) {\n const {\n accountID = process.env.JAZZ_WORKER_ACCOUNT,\n accountSecret = process.env.JAZZ_WORKER_SECRET,\n syncServer = \"wss://cloud.jazz.tools\",\n AccountSchema = Account as unknown as S,\n skipInboxLoad = false,\n asActiveAccount = true,\n } = options;\n\n let node: LocalNode | undefined = undefined;\n\n const peers: Peer[] = [];\n\n // If a peer is provided directly, use it instead of WebSocket\n let wsPeer: WebSocketPeerWithReconnection | undefined;\n\n if (options.peer) {\n peers.push(options.peer);\n } else {\n wsPeer = new WebSocketPeerWithReconnection({\n peer: syncServer,\n reconnectionTimeout: 100,\n addPeer: (peer) => {\n if (node) {\n node.syncManager.addPeer(peer);\n } else {\n peers.push(peer);\n }\n },\n removePeer: () => {},\n WebSocketConstructor: options.WebSocket,\n });\n\n wsPeer.enable();\n }\n\n if (!accountID) {\n throw new Error(\"No accountID provided\");\n }\n if (!accountSecret) {\n throw new Error(\"No accountSecret provided\");\n }\n if (!accountID.startsWith(\"co_\")) {\n throw new Error(\"Invalid accountID\");\n }\n if (!accountSecret?.startsWith(\"sealerSecret_\")) {\n throw new Error(\"Invalid accountSecret\");\n }\n\n const context = await createJazzContextFromExistingCredentials({\n credentials: {\n accountID: accountID,\n secret: accountSecret as AgentSecret,\n },\n AccountSchema,\n sessionProvider: new MockSessionProvider(),\n peers,\n crypto: options.crypto ?? (await WasmCrypto.create()),\n asActiveAccount,\n storage: options.storage,\n });\n\n const account = context.account as InstanceOfSchema<S>;\n node = account.$jazz.localNode;\n\n if (!account.$jazz.refs.profile?.id) {\n throw new Error(\"Account has no profile\");\n }\n\n const inbox = skipInboxLoad ? undefined : await Inbox.load(account);\n\n async function done() {\n await context.account.$jazz.waitForAllCoValuesSync();\n\n wsPeer?.disable();\n context.done();\n }\n\n const inboxPublicApi = inbox\n ? {\n subscribe: inbox.subscribe.bind(inbox) as Inbox[\"subscribe\"],\n }\n : {\n subscribe: () => {},\n };\n\n return {\n /**\n * The worker account instance.\n */\n worker: context.account as Loaded<S>,\n experimental: {\n /**\n * API to subscribe to the inbox messages.\n *\n * More info on the Inbox API: https://jazz.tools/docs/react/server-side/inbox\n */\n inbox: inboxPublicApi,\n },\n /**\n * Wait for the connection to the sync server to be established.\n *\n * If already connected, it will resolve immediately.\n * Returns immediately if using a custom peer.\n */\n waitForConnection() {\n return wsPeer?.waitUntilConnected() ?? Promise.resolve();\n },\n subscribeToConnectionChange(listener: (connected: boolean) => void) {\n if (!wsPeer) {\n // For custom peers, immediately notify as connected\n listener(true);\n return () => {};\n }\n\n wsPeer.subscribe(listener);\n\n return () => {\n wsPeer.unsubscribe(listener);\n };\n },\n /**\n * Waits for all CoValues to sync and then shuts down the worker.\n *\n * To only wait for sync use worker.$jazz.waitForAllCoValuesSync()\n *\n * @deprecated Use shutdownWorker\n */\n done,\n /**\n * Waits for all CoValues to sync and then shuts down the worker.\n *\n * To only wait for sync use worker.$jazz.waitForAllCoValuesSync()\n */\n shutdownWorker() {\n return done();\n },\n };\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -211,7 +211,7 @@
211
211
  },
212
212
  "type": "module",
213
213
  "license": "MIT",
214
- "version": "0.19.21",
214
+ "version": "0.19.22",
215
215
  "dependencies": {
216
216
  "@manuscripts/prosemirror-recreate-steps": "^0.1.4",
217
217
  "@scure/base": "1.2.1",
@@ -228,9 +228,9 @@
228
228
  "prosemirror-transform": "^1.9.0",
229
229
  "use-sync-external-store": "^1.5.0",
230
230
  "zod": "4.1.11",
231
- "cojson": "0.19.21",
232
- "cojson-storage-indexeddb": "0.19.21",
233
- "cojson-transport-ws": "0.19.21"
231
+ "cojson": "0.19.22",
232
+ "cojson-storage-indexeddb": "0.19.22",
233
+ "cojson-transport-ws": "0.19.22"
234
234
  },
235
235
  "devDependencies": {
236
236
  "@scure/bip39": "^1.3.0",
@@ -1,8 +1,7 @@
1
- import { NativeModules } from "react-native";
2
1
  import type ImageResizerType from "@bam.tech/react-native-image-resizer";
3
2
  import type ImageManipulatorType from "expo-image-manipulator";
4
3
  import type { Account, Group } from "jazz-tools";
5
- import { FileStream } from "jazz-tools";
4
+ import { co, type FileStream } from "jazz-tools";
6
5
  import { Image } from "react-native";
7
6
  import { createImageFactory } from "../create-image-factory";
8
7
 
@@ -108,7 +107,8 @@ async function getPlaceholderBase64(filePath: string): Promise<string> {
108
107
  );
109
108
  }
110
109
 
111
- return base64;
110
+ // Convert base64 to data URL
111
+ return "data:image/png;base64," + base64;
112
112
  }
113
113
  }
114
114
 
@@ -147,7 +147,7 @@ async function resize(
147
147
  }
148
148
  }
149
149
 
150
- function getMimeType(filePath: string): Promise<string> {
150
+ async function getMimeType(filePath: string): Promise<string> {
151
151
  return fetch(filePath)
152
152
  .then((res) => res.blob())
153
153
  .then((blob) => blob.type);
@@ -167,9 +167,11 @@ export async function createFileStreamFromSource(
167
167
  const blob = await fetch(filePath).then((res) => res.blob());
168
168
  const arrayBuffer = await toArrayBuffer(blob);
169
169
 
170
- return FileStream.createFromArrayBuffer(arrayBuffer, blob.type, undefined, {
171
- owner,
172
- });
170
+ return co
171
+ .fileStream()
172
+ .createFromArrayBuffer(arrayBuffer, blob.type, undefined, {
173
+ owner,
174
+ });
173
175
  }
174
176
 
175
177
  // TODO: look for more efficient way to do this as React Native hasn't blob.arrayBuffer()
@@ -162,8 +162,8 @@ describe("createImage", async () => {
162
162
  expect(image).toBeDefined();
163
163
  expect(image.originalSize).toEqual([1920, 400]);
164
164
  expect(image.placeholderDataURL).not.toBeDefined();
165
-
166
165
  expect(image[`256x53`]).toBeDefined();
166
+ expect(image[`512x107`]).toBeDefined();
167
167
  expect(image[`1024x213`]).toBeDefined();
168
168
  expect(image[`2048x427`]).not.toBeDefined();
169
169
 
@@ -154,7 +154,7 @@ async function createImage<TSourceType, TResizeOutput>(
154
154
  if (options?.progressive) {
155
155
  imageCoValue.$jazz.set("progressive", true);
156
156
 
157
- const resizes = ([256, 1024, 2048] as const).filter(
157
+ const resizes = ([256, 512, 1024, 2048] as const).filter(
158
158
  (s) =>
159
159
  s <
160
160
  Math.max(imageCoValue.originalSize[0], imageCoValue.originalSize[1]),
@@ -111,8 +111,8 @@ export async function createAsyncStorage() {
111
111
  new LibSQLSqliteAsyncDriver(":memory:"),
112
112
  );
113
113
 
114
- onTestFinished(() => {
115
- storage.close();
114
+ onTestFinished(async () => {
115
+ await storage.close();
116
116
  });
117
117
 
118
118
  return storage;
@@ -144,3 +144,8 @@ export * from "./ssr/index.js";
144
144
  export { captureStack } from "./subscribe/errorReporting.js";
145
145
 
146
146
  export * as jazzConfig from "./config.js";
147
+
148
+ export {
149
+ JazzMessageChannel as experimental_JazzMessageChannel,
150
+ type JazzMessageChannelExposeOptions,
151
+ } from "../worker/JazzMessageChannel.js";
@@ -50,8 +50,8 @@ export async function createAsyncStorage({ filename }: { filename?: string }) {
50
50
  new LibSQLSqliteAsyncDriver(getDbPath(filename)),
51
51
  );
52
52
 
53
- onTestFinished(() => {
54
- storage.close();
53
+ onTestFinished(async () => {
54
+ await storage.close();
55
55
  });
56
56
 
57
57
  return storage;
@@ -0,0 +1,73 @@
1
+ import { CojsonMessageChannel, type Peer } from "cojson";
2
+ import type {
3
+ WaitForConnectionOptions,
4
+ ExposeOptions,
5
+ PostMessageTarget,
6
+ } from "cojson/src/CojsonMessageChannel/types.js";
7
+ import { Account, AnonymousJazzAgent } from "jazz-tools";
8
+ import { activeAccountContext } from "../tools/implementation/activeAccountContext.js";
9
+
10
+ /**
11
+ * Options for JazzMessageChannel.expose()
12
+ */
13
+ export interface JazzMessageChannelExposeOptions extends ExposeOptions {
14
+ /**
15
+ * The account or anonymous agent to use for the connection.
16
+ * If not provided, falls back to the active account context.
17
+ */
18
+ loadAs?: Account | AnonymousJazzAgent;
19
+ }
20
+
21
+ /**
22
+ * JazzMessageChannel provides a high-level API for creating Jazz connections
23
+ * via the MessageChannel API. It wraps cojson's CojsonMessageChannel and
24
+ * automatically manages the node connection.
25
+ */
26
+ export class JazzMessageChannel {
27
+ /**
28
+ * Expose a Jazz connection to a target.
29
+ * This is the host-side API, typically called from the main thread.
30
+ *
31
+ * @param target - Any object with a postMessage method (Worker, Window, etc.)
32
+ * @param opts - Configuration options including the account to use
33
+ * @returns A promise that resolves once the connection is established
34
+ */
35
+ static async expose(
36
+ target: PostMessageTarget,
37
+ opts: JazzMessageChannelExposeOptions = {},
38
+ ): Promise<void> {
39
+ const { loadAs, ...cojsonOpts } = opts;
40
+
41
+ // Get account from loadAs or fall back to active account context
42
+ const accountOrAgent = loadAs ?? activeAccountContext.maybeGet();
43
+
44
+ if (!accountOrAgent) {
45
+ throw new Error(
46
+ "No account provided and no active account context available",
47
+ );
48
+ }
49
+
50
+ const node =
51
+ accountOrAgent instanceof AnonymousJazzAgent
52
+ ? accountOrAgent.node
53
+ : accountOrAgent.$jazz.localNode;
54
+
55
+ const peer = await CojsonMessageChannel.expose(target, cojsonOpts);
56
+
57
+ node.syncManager.addPeer(peer);
58
+ }
59
+
60
+ /**
61
+ * Accept an incoming Jazz connection.
62
+ * Same as cojson CojsonMessageChannel.waitForConnection().
63
+ */
64
+ static waitForConnection(opts?: WaitForConnectionOptions): Promise<Peer> {
65
+ return CojsonMessageChannel.waitForConnection(opts);
66
+ }
67
+ }
68
+
69
+ // Re-export types for convenience
70
+ export type {
71
+ WaitForConnectionOptions,
72
+ AcceptFromPortOptions,
73
+ } from "cojson/src/CojsonMessageChannel/types.js";
@@ -29,6 +29,11 @@ type WorkerOptions<
29
29
  > = {
30
30
  accountID?: string;
31
31
  accountSecret?: string;
32
+ /**
33
+ * A peer to connect to for synchronization.
34
+ * If provided, syncServer is ignored.
35
+ */
36
+ peer?: Peer;
32
37
  syncServer?: string;
33
38
  WebSocket?: AnyWebSocketConstructor;
34
39
  AccountSchema?: S;
@@ -63,21 +68,28 @@ export async function startWorker<
63
68
 
64
69
  const peers: Peer[] = [];
65
70
 
66
- const wsPeer = new WebSocketPeerWithReconnection({
67
- peer: syncServer,
68
- reconnectionTimeout: 100,
69
- addPeer: (peer) => {
70
- if (node) {
71
- node.syncManager.addPeer(peer);
72
- } else {
73
- peers.push(peer);
74
- }
75
- },
76
- removePeer: () => {},
77
- WebSocketConstructor: options.WebSocket,
78
- });
79
-
80
- wsPeer.enable();
71
+ // If a peer is provided directly, use it instead of WebSocket
72
+ let wsPeer: WebSocketPeerWithReconnection | undefined;
73
+
74
+ if (options.peer) {
75
+ peers.push(options.peer);
76
+ } else {
77
+ wsPeer = new WebSocketPeerWithReconnection({
78
+ peer: syncServer,
79
+ reconnectionTimeout: 100,
80
+ addPeer: (peer) => {
81
+ if (node) {
82
+ node.syncManager.addPeer(peer);
83
+ } else {
84
+ peers.push(peer);
85
+ }
86
+ },
87
+ removePeer: () => {},
88
+ WebSocketConstructor: options.WebSocket,
89
+ });
90
+
91
+ wsPeer.enable();
92
+ }
81
93
 
82
94
  if (!accountID) {
83
95
  throw new Error("No accountID provided");
@@ -117,7 +129,7 @@ export async function startWorker<
117
129
  async function done() {
118
130
  await context.account.$jazz.waitForAllCoValuesSync();
119
131
 
120
- wsPeer.disable();
132
+ wsPeer?.disable();
121
133
  context.done();
122
134
  }
123
135
 
@@ -146,11 +158,18 @@ export async function startWorker<
146
158
  * Wait for the connection to the sync server to be established.
147
159
  *
148
160
  * If already connected, it will resolve immediately.
161
+ * Returns immediately if using a custom peer.
149
162
  */
150
163
  waitForConnection() {
151
- return wsPeer.waitUntilConnected();
164
+ return wsPeer?.waitUntilConnected() ?? Promise.resolve();
152
165
  },
153
166
  subscribeToConnectionChange(listener: (connected: boolean) => void) {
167
+ if (!wsPeer) {
168
+ // For custom peers, immediately notify as connected
169
+ listener(true);
170
+ return () => {};
171
+ }
172
+
154
173
  wsPeer.subscribe(listener);
155
174
 
156
175
  return () => {