jazz-tools 0.20.17 → 0.20.18
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/.svelte-kit/__package__/client.d.ts.map +1 -1
- package/.svelte-kit/__package__/client.js +8 -1
- package/.svelte-kit/__package__/tests/client.test.js +67 -0
- package/.turbo/turbo-build.log +63 -63
- package/CHANGELOG.md +13 -0
- package/dist/better-auth/auth/client.d.ts.map +1 -1
- package/dist/better-auth/auth/client.js +12 -1
- package/dist/better-auth/auth/client.js.map +1 -1
- package/dist/browser/BrowserContextManager.d.ts +1 -0
- package/dist/browser/BrowserContextManager.d.ts.map +1 -1
- package/dist/browser/createBrowserContext.d.ts +1 -0
- package/dist/browser/createBrowserContext.d.ts.map +1 -1
- package/dist/browser/index.js +11 -4
- package/dist/browser/index.js.map +1 -1
- package/dist/{chunk-3BV3JUMV.js → chunk-MIPBSAS7.js} +57 -17
- package/dist/chunk-MIPBSAS7.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/react/index.js +11 -3
- package/dist/react/index.js.map +1 -1
- package/dist/react/provider.d.ts +1 -1
- package/dist/react/provider.d.ts.map +1 -1
- package/dist/react-native/index.js +22 -7
- package/dist/react-native/index.js.map +1 -1
- package/dist/react-native-core/ReactNativeContextManager.d.ts +1 -0
- package/dist/react-native-core/ReactNativeContextManager.d.ts.map +1 -1
- package/dist/react-native-core/index.js +22 -7
- package/dist/react-native-core/index.js.map +1 -1
- package/dist/react-native-core/platform.d.ts +1 -0
- package/dist/react-native-core/platform.d.ts.map +1 -1
- package/dist/react-native-core/provider.d.ts +1 -1
- package/dist/react-native-core/provider.d.ts.map +1 -1
- package/dist/svelte/Provider.svelte +3 -0
- package/dist/svelte/Provider.svelte.d.ts.map +1 -1
- package/dist/testing.js +1 -1
- package/dist/tools/coValues/schemaUnion.d.ts +9 -0
- package/dist/tools/coValues/schemaUnion.d.ts.map +1 -1
- package/dist/tools/implementation/createContext.d.ts +7 -3
- package/dist/tools/implementation/createContext.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/unionUtils.d.ts.map +1 -1
- package/dist/tools/subscribe/SubscriptionScope.d.ts.map +1 -1
- package/dist/worker/index.d.ts +1 -0
- package/dist/worker/index.d.ts.map +1 -1
- package/dist/worker/index.js +6 -2
- package/dist/worker/index.js.map +1 -1
- package/package.json +4 -4
- package/src/better-auth/auth/client.ts +15 -1
- package/src/better-auth/auth/tests/client.test.ts +92 -0
- package/src/browser/BrowserContextManager.ts +5 -0
- package/src/browser/createBrowserContext.ts +8 -0
- package/src/react/provider.tsx +9 -1
- package/src/react-native-core/ReactNativeContextManager.ts +5 -0
- package/src/react-native-core/platform.ts +8 -0
- package/src/react-native-core/provider.tsx +9 -1
- package/src/svelte/Provider.svelte +3 -0
- package/src/tools/coValues/schemaUnion.ts +13 -0
- package/src/tools/implementation/createContext.ts +14 -0
- package/src/tools/implementation/zodSchema/schemaTypes/CoMapSchema.ts +24 -8
- package/src/tools/implementation/zodSchema/unionUtils.ts +2 -1
- package/src/tools/subscribe/SubscriptionScope.ts +23 -1
- package/src/tools/tests/coMap.test.ts +30 -0
- package/src/tools/tests/createContext.test.ts +64 -0
- package/src/tools/tests/schemaUnion.test.ts +19 -0
- package/src/worker/index.ts +6 -0
- package/dist/chunk-3BV3JUMV.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SubscriptionScope.d.ts","sourceRoot":"","sources":["../../../src/tools/subscribe/SubscriptionScope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAwC,MAAM,QAAQ,CAAC;AACzE,OAAO,EAIL,KAAK,OAAO,EACZ,WAAW,EACX,SAAS,EACT,KAAK,UAAU,EACf,KAAK,aAAa,
|
|
1
|
+
{"version":3,"file":"SubscriptionScope.d.ts","sourceRoot":"","sources":["../../../src/tools/subscribe/SubscriptionScope.ts"],"names":[],"mappings":"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"}
|
package/dist/worker/index.d.ts
CHANGED
|
@@ -22,6 +22,7 @@ type WorkerOptions<S extends (AccountClass<Account> & CoValueFromRaw<Account>) |
|
|
|
22
22
|
*/
|
|
23
23
|
asActiveAccount?: boolean;
|
|
24
24
|
storage?: StorageAPI;
|
|
25
|
+
experimental_clockSyncFromServerPings?: boolean;
|
|
25
26
|
};
|
|
26
27
|
/** @category Context Creation */
|
|
27
28
|
export declare function startWorker<S extends (AccountClass<Account> & CoValueFromRaw<Account>) | AnyAccountSchema>(options: WorkerOptions<S>): Promise<{
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/worker/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,cAAc,EAEd,IAAI,EACJ,UAAU,EACX,MAAM,QAAQ,CAAC;AAChB,OAAO,EACL,KAAK,uBAAuB,EAE7B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,OAAO,EACP,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,KAAK,EAEL,MAAM,EAGP,MAAM,YAAY,CAAC;AAEpB,KAAK,aAAa,CAChB,CAAC,SACG,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC,GACjD,gBAAgB,IAClB;IACF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,uBAAuB,CAAC;IACpC,aAAa,CAAC,EAAE,CAAC,CAAC;IAClB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB;;OAEG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,OAAO,CAAC,EAAE,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/worker/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,cAAc,EAEd,IAAI,EACJ,UAAU,EACX,MAAM,QAAQ,CAAC;AAChB,OAAO,EACL,KAAK,uBAAuB,EAE7B,MAAM,qBAAqB,CAAC;AAE7B,OAAO,EACL,OAAO,EACP,YAAY,EACZ,gBAAgB,EAChB,cAAc,EACd,KAAK,EAEL,MAAM,EAGP,MAAM,YAAY,CAAC;AAEpB,KAAK,aAAa,CAChB,CAAC,SACG,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC,GACjD,gBAAgB,IAClB;IACF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,uBAAuB,CAAC;IACpC,aAAa,CAAC,EAAE,CAAC,CAAC;IAClB,MAAM,CAAC,EAAE,cAAc,CAAC;IACxB;;OAEG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB,qCAAqC,CAAC,EAAE,OAAO,CAAC;CACjD,CAAC;AAEF,iCAAiC;AACjC,wBAAsB,WAAW,CAC/B,CAAC,SACG,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,cAAc,CAAC,OAAO,CAAC,CAAC,GACjD,gBAAgB,EACpB,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC;IA6FvB;;OAEG;YACwB,MAAM,CAAC,CAAC,CAAC;;QAElC;;;;WAIG;;uBAhByC,KAAK,CAAC,WAAW,CAAC;;;;;IAmBhE;;;;;OAKG;;0CAImC,CAAC,SAAS,EAAE,OAAO,KAAK,IAAI;IAalE;;;;;;OAMG;;IAEH;;;;OAIG;;GAKN"}
|
package/dist/worker/index.js
CHANGED
|
@@ -36,7 +36,10 @@ async function startWorker(options) {
|
|
|
36
36
|
},
|
|
37
37
|
removePeer: () => {
|
|
38
38
|
},
|
|
39
|
-
WebSocketConstructor: options.WebSocket
|
|
39
|
+
WebSocketConstructor: options.WebSocket,
|
|
40
|
+
onPingReceived: (sample) => {
|
|
41
|
+
node?.clockOffset.addSample(sample);
|
|
42
|
+
}
|
|
40
43
|
});
|
|
41
44
|
wsPeer.enable();
|
|
42
45
|
}
|
|
@@ -62,7 +65,8 @@ async function startWorker(options) {
|
|
|
62
65
|
peers,
|
|
63
66
|
crypto: options.crypto ?? await WasmCrypto.create(),
|
|
64
67
|
asActiveAccount,
|
|
65
|
-
storage: options.storage
|
|
68
|
+
storage: options.storage,
|
|
69
|
+
experimental_clockSyncFromServerPings: options.experimental_clockSyncFromServerPings
|
|
66
70
|
});
|
|
67
71
|
const account = context.account;
|
|
68
72
|
node = account.$jazz.localNode;
|
package/dist/worker/index.js.map
CHANGED
|
@@ -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 /**\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":";AAOA;AAAA,EAEE;AAAA,OACK;AACP,SAAS,kBAAkB;AAC3B;AAAA,EACE;AAAA,EAIA;AAAA,EAGA;AAAA,EACA;AAAA,OACK;
|
|
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 experimental_clockSyncFromServerPings?: boolean;\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 onPingReceived: (sample) => {\n node?.clockOffset.addSample(sample);\n },\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 experimental_clockSyncFromServerPings:\n options.experimental_clockSyncFromServerPings,\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
|
@@ -206,7 +206,7 @@
|
|
|
206
206
|
},
|
|
207
207
|
"type": "module",
|
|
208
208
|
"license": "MIT",
|
|
209
|
-
"version": "0.20.
|
|
209
|
+
"version": "0.20.18",
|
|
210
210
|
"dependencies": {
|
|
211
211
|
"@scure/base": "1.2.1",
|
|
212
212
|
"@scure/bip39": "^1.3.0",
|
|
@@ -223,9 +223,9 @@
|
|
|
223
223
|
"prosemirror-transform": "^1.9.0",
|
|
224
224
|
"use-sync-external-store": "^1.5.0",
|
|
225
225
|
"zod": "4.1.11",
|
|
226
|
-
"cojson": "0.20.
|
|
227
|
-
"cojson-storage-indexeddb": "0.20.
|
|
228
|
-
"cojson-transport-ws": "0.20.
|
|
226
|
+
"cojson": "0.20.18",
|
|
227
|
+
"cojson-storage-indexeddb": "0.20.18",
|
|
228
|
+
"cojson-transport-ws": "0.20.18"
|
|
229
229
|
},
|
|
230
230
|
"devDependencies": {
|
|
231
231
|
"@scure/bip39": "^1.3.0",
|
|
@@ -26,8 +26,10 @@ export const jazzPluginClient = () => {
|
|
|
26
26
|
let jazzContext: JazzContextType<Account>;
|
|
27
27
|
let authSecretStorage: AuthSecretStorage;
|
|
28
28
|
let signOutUnsubscription: () => void;
|
|
29
|
+
let authGeneration = 0;
|
|
29
30
|
|
|
30
31
|
const authenticateOnJazz = async (jazzAuth: AuthSetPayload) => {
|
|
32
|
+
authGeneration++;
|
|
31
33
|
const parsedJazzAuth = {
|
|
32
34
|
...jazzAuth,
|
|
33
35
|
secretSeed: jazzAuth.secretSeed
|
|
@@ -89,6 +91,12 @@ export const jazzPluginClient = () => {
|
|
|
89
91
|
name: "jazz-plugin",
|
|
90
92
|
hooks: {
|
|
91
93
|
async onRequest(context) {
|
|
94
|
+
if (context.url.toString().includes("/get-session")) {
|
|
95
|
+
context.headers.set(
|
|
96
|
+
"x-jazz-auth-generation",
|
|
97
|
+
String(authGeneration),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
92
100
|
if (
|
|
93
101
|
SIGNUP_URLS.some((url) => context.url.toString().includes(url))
|
|
94
102
|
) {
|
|
@@ -123,7 +131,13 @@ export const jazzPluginClient = () => {
|
|
|
123
131
|
|
|
124
132
|
if (context.request.url.toString().includes("/get-session")) {
|
|
125
133
|
if (context.data === null) {
|
|
126
|
-
|
|
134
|
+
const requestAuthGeneration = Number(
|
|
135
|
+
context.request.headers.get("x-jazz-auth-generation") ?? "0",
|
|
136
|
+
);
|
|
137
|
+
if (
|
|
138
|
+
authSecretStorage.isAuthenticated === true &&
|
|
139
|
+
requestAuthGeneration === authGeneration
|
|
140
|
+
) {
|
|
127
141
|
console.info(
|
|
128
142
|
"Jazz is authenticated, but the session is null. Logging out",
|
|
129
143
|
);
|
|
@@ -610,6 +610,98 @@ describe("Better-Auth client plugin", () => {
|
|
|
610
610
|
expect(customFetchImpl).toHaveBeenCalledTimes(3);
|
|
611
611
|
});
|
|
612
612
|
|
|
613
|
+
it("should NOT logout from Jazz if a stale null get-session arrives after authentication", async () => {
|
|
614
|
+
const credentials = await authSecretStorage.get();
|
|
615
|
+
assert(credentials, "Jazz credentials are not available");
|
|
616
|
+
|
|
617
|
+
// This test validates race condition handling where a stale null response
|
|
618
|
+
// arrives after authentication. The authGeneration is tracked via the
|
|
619
|
+
// x-jazz-auth-generation header set in onRequest and read in onSuccess.
|
|
620
|
+
// Since authGeneration increments after sign-in, the stale response's
|
|
621
|
+
// generation number will be older, preventing an unintended logout.
|
|
622
|
+
|
|
623
|
+
const capturedHeaders: any[] = [];
|
|
624
|
+
let resolveStaleSession: (value: Response) => void;
|
|
625
|
+
const staleSessionPromise = new Promise<Response>((resolve) => {
|
|
626
|
+
resolveStaleSession = resolve;
|
|
627
|
+
});
|
|
628
|
+
|
|
629
|
+
customFetchImpl.mockImplementation(
|
|
630
|
+
(urlOrRequest: string | Request, init?: RequestInit) => {
|
|
631
|
+
const headers =
|
|
632
|
+
urlOrRequest instanceof Request
|
|
633
|
+
? urlOrRequest.headers
|
|
634
|
+
: init?.headers || {};
|
|
635
|
+
capturedHeaders.push(headers);
|
|
636
|
+
|
|
637
|
+
const url =
|
|
638
|
+
urlOrRequest instanceof Request ? urlOrRequest.url : urlOrRequest;
|
|
639
|
+
|
|
640
|
+
if (url.toString().includes("/get-session")) {
|
|
641
|
+
return staleSessionPromise;
|
|
642
|
+
}
|
|
643
|
+
return Promise.resolve(
|
|
644
|
+
new Response(
|
|
645
|
+
JSON.stringify({
|
|
646
|
+
user: {
|
|
647
|
+
id: "user-1",
|
|
648
|
+
email: "test@jazz.dev",
|
|
649
|
+
name: "Matteo",
|
|
650
|
+
},
|
|
651
|
+
jazzAuth: {
|
|
652
|
+
accountID: credentials.accountID,
|
|
653
|
+
secretSeed: credentials.secretSeed,
|
|
654
|
+
accountSecret: credentials.accountSecret,
|
|
655
|
+
provider: "better-auth",
|
|
656
|
+
},
|
|
657
|
+
}),
|
|
658
|
+
),
|
|
659
|
+
);
|
|
660
|
+
},
|
|
661
|
+
);
|
|
662
|
+
|
|
663
|
+
const staleSessionFetch = authClient.getSession();
|
|
664
|
+
|
|
665
|
+
// We need to wait a tick for onRequest to be called and customFetchImpl to be triggered
|
|
666
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
667
|
+
|
|
668
|
+
const getHeader = (headers: any, name: string) => {
|
|
669
|
+
if (!headers) return null;
|
|
670
|
+
if (typeof headers.get === "function") return headers.get(name);
|
|
671
|
+
if (Array.isArray(headers)) {
|
|
672
|
+
const pair = (headers as string[][]).find(
|
|
673
|
+
(h) => h[0]?.toLowerCase() === name.toLowerCase(),
|
|
674
|
+
);
|
|
675
|
+
return pair ? pair[1] : null;
|
|
676
|
+
}
|
|
677
|
+
return headers[name] || headers[name.toLowerCase()] || null;
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
// Verify the stale request had authGeneration=0 in the header
|
|
681
|
+
expect(getHeader(capturedHeaders[0], "x-jazz-auth-generation")).toBe("0");
|
|
682
|
+
|
|
683
|
+
// Now sign in, which increments authGeneration to 1
|
|
684
|
+
await authClient.signIn.email({
|
|
685
|
+
email: "test@jazz.dev",
|
|
686
|
+
password: "password",
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
expect(authSecretStorage.isAuthenticated).toBe(true);
|
|
690
|
+
|
|
691
|
+
// Verify the sign-in request had no x-jazz-auth-generation header (it is gated to get-session)
|
|
692
|
+
expect(
|
|
693
|
+
getHeader(capturedHeaders[1], "x-jazz-auth-generation"),
|
|
694
|
+
).toBeNull();
|
|
695
|
+
|
|
696
|
+
// Resolve the stale get-session response with null
|
|
697
|
+
resolveStaleSession!(new Response(JSON.stringify(null)));
|
|
698
|
+
await staleSessionFetch;
|
|
699
|
+
|
|
700
|
+
// The stale null response should not trigger a logout because its
|
|
701
|
+
// authGeneration (0) < current authGeneration (1), so >= comparison fails
|
|
702
|
+
expect(authSecretStorage.isAuthenticated).toBe(true);
|
|
703
|
+
});
|
|
704
|
+
|
|
613
705
|
it("should deduplicate auth requests for the same account", async () => {
|
|
614
706
|
const credentials = await authSecretStorage.get();
|
|
615
707
|
assert(credentials, "Jazz credentials are not available");
|
|
@@ -31,6 +31,7 @@ export type JazzContextManagerProps<
|
|
|
31
31
|
storage?: BaseBrowserContextOptions["storage"];
|
|
32
32
|
AccountSchema?: S;
|
|
33
33
|
defaultProfileName?: string;
|
|
34
|
+
experimental_clockSyncFromServerPings?: boolean;
|
|
34
35
|
};
|
|
35
36
|
|
|
36
37
|
export class JazzBrowserContextManager<
|
|
@@ -57,6 +58,8 @@ export class JazzBrowserContextManager<
|
|
|
57
58
|
sync: props.sync,
|
|
58
59
|
storage: props.storage,
|
|
59
60
|
authSecretStorage: this.authSecretStorage,
|
|
61
|
+
experimental_clockSyncFromServerPings:
|
|
62
|
+
props.experimental_clockSyncFromServerPings,
|
|
60
63
|
});
|
|
61
64
|
} else {
|
|
62
65
|
return createJazzBrowserContext<S>({
|
|
@@ -67,6 +70,8 @@ export class JazzBrowserContextManager<
|
|
|
67
70
|
newAccountProps: authProps?.newAccountProps,
|
|
68
71
|
defaultProfileName: props.defaultProfileName,
|
|
69
72
|
authSecretStorage: this.authSecretStorage,
|
|
73
|
+
experimental_clockSyncFromServerPings:
|
|
74
|
+
props.experimental_clockSyncFromServerPings,
|
|
70
75
|
});
|
|
71
76
|
}
|
|
72
77
|
}
|
|
@@ -30,6 +30,7 @@ export type BaseBrowserContextOptions = {
|
|
|
30
30
|
storage?: "indexedDB";
|
|
31
31
|
crypto?: CryptoProvider;
|
|
32
32
|
authSecretStorage: AuthSecretStorage;
|
|
33
|
+
experimental_clockSyncFromServerPings?: boolean;
|
|
33
34
|
};
|
|
34
35
|
|
|
35
36
|
class BrowserWebSocketPeerWithReconnection extends WebSocketPeerWithReconnection {
|
|
@@ -79,6 +80,9 @@ async function setupPeers(options: BaseBrowserContextOptions) {
|
|
|
79
80
|
removePeer: (peer) => {
|
|
80
81
|
peers.splice(peers.indexOf(peer), 1);
|
|
81
82
|
},
|
|
83
|
+
onPingReceived: (sample) => {
|
|
84
|
+
node?.clockOffset.addSample(sample);
|
|
85
|
+
},
|
|
82
86
|
});
|
|
83
87
|
|
|
84
88
|
function toggleNetwork(enabled: boolean) {
|
|
@@ -136,6 +140,8 @@ export async function createJazzBrowserGuestContext(
|
|
|
136
140
|
peers,
|
|
137
141
|
syncWhen,
|
|
138
142
|
storage,
|
|
143
|
+
experimental_clockSyncFromServerPings:
|
|
144
|
+
options.experimental_clockSyncFromServerPings,
|
|
139
145
|
});
|
|
140
146
|
|
|
141
147
|
setNode(context.agent.node);
|
|
@@ -214,6 +220,8 @@ export async function createJazzBrowserContext<
|
|
|
214
220
|
AccountSchema: options.AccountSchema,
|
|
215
221
|
sessionProvider: getBrowserLockSessionProvider(),
|
|
216
222
|
authSecretStorage: options.authSecretStorage,
|
|
223
|
+
experimental_clockSyncFromServerPings:
|
|
224
|
+
options.experimental_clockSyncFromServerPings,
|
|
217
225
|
});
|
|
218
226
|
|
|
219
227
|
setNode(context.node);
|
package/src/react/provider.tsx
CHANGED
|
@@ -47,6 +47,7 @@ export function JazzReactProvider<
|
|
|
47
47
|
enableSSR,
|
|
48
48
|
fallback = null,
|
|
49
49
|
authSecretStorageKey,
|
|
50
|
+
experimental_clockSyncFromServerPings,
|
|
50
51
|
}: JazzProviderProps<S>) {
|
|
51
52
|
if (useContext(JazzContext)) {
|
|
52
53
|
throw new Error(
|
|
@@ -82,8 +83,15 @@ export function JazzReactProvider<
|
|
|
82
83
|
onAnonymousAccountDiscarded: onAnonymousAccountDiscarded
|
|
83
84
|
? onAnonymousAccountDiscardedRefCallback
|
|
84
85
|
: undefined,
|
|
86
|
+
experimental_clockSyncFromServerPings,
|
|
85
87
|
} satisfies JazzContextManagerProps<S>;
|
|
86
|
-
}, [
|
|
88
|
+
}, [
|
|
89
|
+
guestMode,
|
|
90
|
+
sync.peer,
|
|
91
|
+
sync.when,
|
|
92
|
+
storage,
|
|
93
|
+
experimental_clockSyncFromServerPings,
|
|
94
|
+
]);
|
|
87
95
|
|
|
88
96
|
if (contextManager.propsChanged(props) && typeof window !== "undefined") {
|
|
89
97
|
contextManager.createContext(props).catch((error) => {
|
|
@@ -33,6 +33,7 @@ export type JazzContextManagerProps<
|
|
|
33
33
|
onAnonymousAccountDiscarded?: (
|
|
34
34
|
anonymousAccount: InstanceOfSchema<S>,
|
|
35
35
|
) => Promise<void>;
|
|
36
|
+
experimental_clockSyncFromServerPings?: boolean;
|
|
36
37
|
};
|
|
37
38
|
|
|
38
39
|
export class ReactNativeContextManager<
|
|
@@ -49,6 +50,8 @@ export class ReactNativeContextManager<
|
|
|
49
50
|
sync: props.sync,
|
|
50
51
|
storage: props.storage,
|
|
51
52
|
authSecretStorage: this.authSecretStorage,
|
|
53
|
+
experimental_clockSyncFromServerPings:
|
|
54
|
+
props.experimental_clockSyncFromServerPings,
|
|
52
55
|
});
|
|
53
56
|
} else {
|
|
54
57
|
return createJazzReactNativeContext<S>({
|
|
@@ -59,6 +62,8 @@ export class ReactNativeContextManager<
|
|
|
59
62
|
newAccountProps: authProps?.newAccountProps,
|
|
60
63
|
defaultProfileName: props.defaultProfileName,
|
|
61
64
|
authSecretStorage: this.authSecretStorage,
|
|
65
|
+
experimental_clockSyncFromServerPings:
|
|
66
|
+
props.experimental_clockSyncFromServerPings,
|
|
62
67
|
});
|
|
63
68
|
}
|
|
64
69
|
}
|
|
@@ -26,6 +26,7 @@ export type BaseReactNativeContextOptions = {
|
|
|
26
26
|
reconnectionTimeout?: number;
|
|
27
27
|
storage?: SQLiteDatabaseDriverAsync | "disabled";
|
|
28
28
|
authSecretStorage: AuthSecretStorage;
|
|
29
|
+
experimental_clockSyncFromServerPings?: boolean;
|
|
29
30
|
};
|
|
30
31
|
|
|
31
32
|
class ReactNativeWebSocketPeerWithReconnection extends WebSocketPeerWithReconnection {
|
|
@@ -73,6 +74,9 @@ async function setupPeers(options: BaseReactNativeContextOptions) {
|
|
|
73
74
|
removePeer: (peer) => {
|
|
74
75
|
peers.splice(peers.indexOf(peer), 1);
|
|
75
76
|
},
|
|
77
|
+
onPingReceived: (sample) => {
|
|
78
|
+
node?.clockOffset.addSample(sample);
|
|
79
|
+
},
|
|
76
80
|
});
|
|
77
81
|
|
|
78
82
|
function toggleNetwork(enabled: boolean) {
|
|
@@ -128,6 +132,8 @@ export async function createJazzReactNativeGuestContext(
|
|
|
128
132
|
peers,
|
|
129
133
|
syncWhen,
|
|
130
134
|
storage,
|
|
135
|
+
experimental_clockSyncFromServerPings:
|
|
136
|
+
options.experimental_clockSyncFromServerPings,
|
|
131
137
|
});
|
|
132
138
|
|
|
133
139
|
setNode(context.agent.node);
|
|
@@ -211,6 +217,8 @@ export async function createJazzReactNativeContext<
|
|
|
211
217
|
sessionProvider,
|
|
212
218
|
authSecretStorage: options.authSecretStorage,
|
|
213
219
|
storage,
|
|
220
|
+
experimental_clockSyncFromServerPings:
|
|
221
|
+
options.experimental_clockSyncFromServerPings,
|
|
214
222
|
});
|
|
215
223
|
|
|
216
224
|
setNode(context.node);
|
|
@@ -47,6 +47,7 @@ export function JazzProviderCore<
|
|
|
47
47
|
kvStore,
|
|
48
48
|
authSecretStorageKey,
|
|
49
49
|
fallback = null,
|
|
50
|
+
experimental_clockSyncFromServerPings,
|
|
50
51
|
}: JazzProviderProps<S>) {
|
|
51
52
|
if (useContext(JazzContext)) {
|
|
52
53
|
throw new Error(
|
|
@@ -80,8 +81,15 @@ export function JazzProviderCore<
|
|
|
80
81
|
onAnonymousAccountDiscarded: onAnonymousAccountDiscarded
|
|
81
82
|
? onAnonymousAccountDiscardedRefCallback
|
|
82
83
|
: undefined,
|
|
84
|
+
experimental_clockSyncFromServerPings,
|
|
83
85
|
} satisfies JazzContextManagerProps<S>;
|
|
84
|
-
}, [
|
|
86
|
+
}, [
|
|
87
|
+
guestMode,
|
|
88
|
+
sync.peer,
|
|
89
|
+
sync.when,
|
|
90
|
+
storage,
|
|
91
|
+
experimental_clockSyncFromServerPings,
|
|
92
|
+
]);
|
|
85
93
|
|
|
86
94
|
if (contextManager.propsChanged(props)) {
|
|
87
95
|
contextManager.createContext(props).catch((error) => {
|
|
@@ -77,6 +77,7 @@
|
|
|
77
77
|
props.sync.peer;
|
|
78
78
|
props.storage;
|
|
79
79
|
props.guestMode;
|
|
80
|
+
props.experimental_clockSyncFromServerPings;
|
|
80
81
|
return untrack(() => {
|
|
81
82
|
if (!props.sync) return;
|
|
82
83
|
|
|
@@ -89,6 +90,8 @@
|
|
|
89
90
|
defaultProfileName: props.defaultProfileName,
|
|
90
91
|
onAnonymousAccountDiscarded: props.onAnonymousAccountDiscarded,
|
|
91
92
|
onLogOut: props.onLogOut,
|
|
93
|
+
experimental_clockSyncFromServerPings:
|
|
94
|
+
props.experimental_clockSyncFromServerPings,
|
|
92
95
|
})
|
|
93
96
|
.catch((error) => {
|
|
94
97
|
console.error("Error creating Jazz browser context:", error);
|
|
@@ -22,6 +22,19 @@ import {
|
|
|
22
22
|
subscribeToCoValueWithoutMe,
|
|
23
23
|
} from "../internal.js";
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Thrown by a discriminated union's `fromRaw` when the stored value doesn't
|
|
27
|
+
* match any declared variant. Caught at the subscription layer so that
|
|
28
|
+
* `load()` can settle as unavailable instead of hanging, since the throw
|
|
29
|
+
* would otherwise disappear into cojson's async update loop.
|
|
30
|
+
*/
|
|
31
|
+
export class SchemaUnionNoMatchingVariantError extends Error {
|
|
32
|
+
constructor(message: string) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.name = "SchemaUnionNoMatchingVariantError";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
/**
|
|
26
39
|
* Extends `SchemaUnion` with a non-abstract constructor.
|
|
27
40
|
*/
|
|
@@ -118,6 +118,7 @@ export async function createJazzContextFromExistingCredentials<
|
|
|
118
118
|
sessionProvider,
|
|
119
119
|
onLogOut,
|
|
120
120
|
asActiveAccount,
|
|
121
|
+
experimental_clockSyncFromServerPings,
|
|
121
122
|
}: {
|
|
122
123
|
credentials: Credentials;
|
|
123
124
|
peers: Peer[];
|
|
@@ -128,6 +129,7 @@ export async function createJazzContextFromExistingCredentials<
|
|
|
128
129
|
onLogOut?: () => void;
|
|
129
130
|
storage?: StorageAPI;
|
|
130
131
|
asActiveAccount: boolean;
|
|
132
|
+
experimental_clockSyncFromServerPings?: boolean;
|
|
131
133
|
}): Promise<JazzContextWithAccount<InstanceOfSchema<S>>> {
|
|
132
134
|
const { sessionID, sessionDone } = await sessionProvider.acquireSession(
|
|
133
135
|
credentials.accountID,
|
|
@@ -149,6 +151,7 @@ export async function createJazzContextFromExistingCredentials<
|
|
|
149
151
|
crypto,
|
|
150
152
|
storage,
|
|
151
153
|
enableFullStorageReconciliation: !!storage,
|
|
154
|
+
experimental_clockSyncFromServerPings,
|
|
152
155
|
migration: async (rawAccount, _node, creationProps) => {
|
|
153
156
|
const account = AccountClass.fromRaw(rawAccount) as InstanceOfSchema<S>;
|
|
154
157
|
if (asActiveAccount) {
|
|
@@ -193,6 +196,7 @@ export async function createJazzContextForNewAccount<
|
|
|
193
196
|
onLogOut,
|
|
194
197
|
storage,
|
|
195
198
|
sessionProvider,
|
|
199
|
+
experimental_clockSyncFromServerPings,
|
|
196
200
|
}: {
|
|
197
201
|
creationProps: { name: string };
|
|
198
202
|
initialAgentSecret?: AgentSecret;
|
|
@@ -203,6 +207,7 @@ export async function createJazzContextForNewAccount<
|
|
|
203
207
|
onLogOut?: () => Promise<void>;
|
|
204
208
|
storage?: StorageAPI;
|
|
205
209
|
sessionProvider: SessionProvider;
|
|
210
|
+
experimental_clockSyncFromServerPings?: boolean;
|
|
206
211
|
}): Promise<JazzContextWithAccount<InstanceOfSchema<S>>> {
|
|
207
212
|
const CurrentAccountSchema =
|
|
208
213
|
PropsAccountSchema ?? (RegisteredSchemas["Account"] as unknown as S);
|
|
@@ -218,6 +223,7 @@ export async function createJazzContextForNewAccount<
|
|
|
218
223
|
initialAgentSecret,
|
|
219
224
|
storage,
|
|
220
225
|
enableFullStorageReconciliation: !!storage,
|
|
226
|
+
experimental_clockSyncFromServerPings,
|
|
221
227
|
migration: async (rawAccount, _node, creationProps) => {
|
|
222
228
|
const account = AccountClass.fromRaw(rawAccount) as InstanceOfSchema<S>;
|
|
223
229
|
activeAccountContext.set(account);
|
|
@@ -263,6 +269,7 @@ export async function createJazzContext<
|
|
|
263
269
|
sessionProvider: SessionProvider;
|
|
264
270
|
authSecretStorage: AuthSecretStorage;
|
|
265
271
|
storage?: StorageAPI;
|
|
272
|
+
experimental_clockSyncFromServerPings?: boolean;
|
|
266
273
|
}) {
|
|
267
274
|
const crypto = options.crypto;
|
|
268
275
|
|
|
@@ -294,6 +301,8 @@ export async function createJazzContext<
|
|
|
294
301
|
},
|
|
295
302
|
storage: options.storage,
|
|
296
303
|
asActiveAccount: true,
|
|
304
|
+
experimental_clockSyncFromServerPings:
|
|
305
|
+
options.experimental_clockSyncFromServerPings,
|
|
297
306
|
});
|
|
298
307
|
} else {
|
|
299
308
|
const secretSeed = options.crypto.newRandomSecretSeed();
|
|
@@ -318,6 +327,8 @@ export async function createJazzContext<
|
|
|
318
327
|
await authSecretStorage.clearWithoutNotify();
|
|
319
328
|
},
|
|
320
329
|
storage: options.storage,
|
|
330
|
+
experimental_clockSyncFromServerPings:
|
|
331
|
+
options.experimental_clockSyncFromServerPings,
|
|
321
332
|
});
|
|
322
333
|
|
|
323
334
|
if (!options.newAccountProps) {
|
|
@@ -341,11 +352,13 @@ export function createAnonymousJazzContext({
|
|
|
341
352
|
syncWhen,
|
|
342
353
|
crypto,
|
|
343
354
|
storage,
|
|
355
|
+
experimental_clockSyncFromServerPings,
|
|
344
356
|
}: {
|
|
345
357
|
peers: Peer[];
|
|
346
358
|
syncWhen?: SyncWhen;
|
|
347
359
|
crypto: CryptoProvider;
|
|
348
360
|
storage?: StorageAPI;
|
|
361
|
+
experimental_clockSyncFromServerPings?: boolean;
|
|
349
362
|
}): JazzContextWithAgent {
|
|
350
363
|
const agentSecret = crypto.newRandomAgentSecret();
|
|
351
364
|
|
|
@@ -355,6 +368,7 @@ export function createAnonymousJazzContext({
|
|
|
355
368
|
crypto,
|
|
356
369
|
syncWhen,
|
|
357
370
|
!!storage,
|
|
371
|
+
{ experimental_clockSyncFromServerPings },
|
|
358
372
|
);
|
|
359
373
|
|
|
360
374
|
for (const peer of peers) {
|
|
@@ -524,11 +524,19 @@ export class CoMapSchema<
|
|
|
524
524
|
Owner,
|
|
525
525
|
DefaultResolveQuery
|
|
526
526
|
> {
|
|
527
|
+
const extendedShape = {} as z.core.util.Extend<Shape, ExtendShape>;
|
|
528
|
+
|
|
529
|
+
Object.defineProperties(
|
|
530
|
+
extendedShape,
|
|
531
|
+
Object.getOwnPropertyDescriptors(this.shape),
|
|
532
|
+
);
|
|
533
|
+
Object.defineProperties(
|
|
534
|
+
extendedShape,
|
|
535
|
+
Object.getOwnPropertyDescriptors(shape),
|
|
536
|
+
);
|
|
537
|
+
|
|
527
538
|
return this.copy({
|
|
528
|
-
shape:
|
|
529
|
-
...this.shape,
|
|
530
|
-
...shape,
|
|
531
|
-
} as z.core.util.Extend<Shape, ExtendShape>,
|
|
539
|
+
shape: extendedShape,
|
|
532
540
|
});
|
|
533
541
|
}
|
|
534
542
|
|
|
@@ -549,11 +557,19 @@ export class CoMapSchema<
|
|
|
549
557
|
Owner,
|
|
550
558
|
DefaultResolveQuery
|
|
551
559
|
> {
|
|
560
|
+
const extendedShape = {} as z.core.util.Extend<Shape, ExtendShape>;
|
|
561
|
+
|
|
562
|
+
Object.defineProperties(
|
|
563
|
+
extendedShape,
|
|
564
|
+
Object.getOwnPropertyDescriptors(this.shape),
|
|
565
|
+
);
|
|
566
|
+
Object.defineProperties(
|
|
567
|
+
extendedShape,
|
|
568
|
+
Object.getOwnPropertyDescriptors(shape),
|
|
569
|
+
);
|
|
570
|
+
|
|
552
571
|
return this.copy({
|
|
553
|
-
shape:
|
|
554
|
-
...this.shape,
|
|
555
|
-
...shape,
|
|
556
|
-
} as z.core.util.Extend<Shape, ExtendShape>,
|
|
572
|
+
shape: extendedShape,
|
|
557
573
|
});
|
|
558
574
|
}
|
|
559
575
|
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
DiscriminableCoValueSchemas,
|
|
9
9
|
DiscriminableCoreCoValueSchema,
|
|
10
10
|
SchemaUnionDiscriminator,
|
|
11
|
+
SchemaUnionNoMatchingVariantError,
|
|
11
12
|
createCoreCoMapSchema,
|
|
12
13
|
} from "../../internal.js";
|
|
13
14
|
import {
|
|
@@ -118,7 +119,7 @@ export function schemaUnionDiscriminatorFor(
|
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
|
|
121
|
-
throw new
|
|
122
|
+
throw new SchemaUnionNoMatchingVariantError(
|
|
122
123
|
"co.discriminatedUnion() of collaborative types with no matching discriminator value found",
|
|
123
124
|
);
|
|
124
125
|
};
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
NotLoaded,
|
|
9
9
|
type RefEncoded,
|
|
10
10
|
type RefsToResolve,
|
|
11
|
+
SchemaUnionNoMatchingVariantError,
|
|
11
12
|
TypeSym,
|
|
12
13
|
createUnloadedCoValue,
|
|
13
14
|
instantiateRefEncodedFromRaw,
|
|
@@ -173,7 +174,28 @@ export class SubscriptionScope<D extends CoValue> {
|
|
|
173
174
|
}
|
|
174
175
|
|
|
175
176
|
this.migrating = true;
|
|
176
|
-
|
|
177
|
+
let instance: CoValue;
|
|
178
|
+
try {
|
|
179
|
+
instance = instantiateRefEncodedFromRaw(this.schema, value);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
// A discriminated union whose stored value doesn't match any
|
|
182
|
+
// declared variant would otherwise hang load() — the throw
|
|
183
|
+
// escapes into cojson's async update loop and nothing settles.
|
|
184
|
+
// Treat it as unavailable so load() resolves. Other
|
|
185
|
+
// instantiation errors (e.g. CoVector dimension mismatch) keep
|
|
186
|
+
// throwing loudly, as they indicate schema drift that callers
|
|
187
|
+
// should surface.
|
|
188
|
+
if (!(error instanceof SchemaUnionNoMatchingVariantError)) {
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
this.migrationFailed = true;
|
|
192
|
+
this.migrated = true;
|
|
193
|
+
console.error(
|
|
194
|
+
`Schema instantiation failed for ${this.id}: ${error.message}`,
|
|
195
|
+
);
|
|
196
|
+
this.handleUpdate(CoValueLoadingState.UNAVAILABLE);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
177
199
|
try {
|
|
178
200
|
applyCoValueMigrations(instance);
|
|
179
201
|
} catch (error) {
|