jazz-tools 0.19.14 → 0.19.16

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 (37) hide show
  1. package/.turbo/turbo-build.log +56 -56
  2. package/CHANGELOG.md +25 -0
  3. package/dist/{chunk-GAPMDNJY.js → chunk-R3KIZG4P.js} +47 -27
  4. package/dist/chunk-R3KIZG4P.js.map +1 -0
  5. package/dist/index.js +22 -14
  6. package/dist/index.js.map +1 -1
  7. package/dist/react-native/index.js +18 -0
  8. package/dist/react-native/index.js.map +1 -1
  9. package/dist/react-native-core/ReactNativeSessionProvider.d.ts.map +1 -1
  10. package/dist/react-native-core/index.js +18 -0
  11. package/dist/react-native-core/index.js.map +1 -1
  12. package/dist/testing.js +1 -1
  13. package/dist/tools/auth/clerk/index.d.ts +1 -0
  14. package/dist/tools/auth/clerk/index.d.ts.map +1 -1
  15. package/dist/tools/auth/clerk/tests/isClerkAuthStateEqual.test.d.ts +2 -0
  16. package/dist/tools/auth/clerk/tests/isClerkAuthStateEqual.test.d.ts.map +1 -0
  17. package/dist/tools/auth/clerk/types.d.ts +2 -2
  18. package/dist/tools/auth/clerk/types.d.ts.map +1 -1
  19. package/dist/tools/coValues/CoValueBase.d.ts +13 -0
  20. package/dist/tools/coValues/CoValueBase.d.ts.map +1 -1
  21. package/dist/tools/subscribe/SubscriptionScope.d.ts +33 -35
  22. package/dist/tools/subscribe/SubscriptionScope.d.ts.map +1 -1
  23. package/package.json +4 -4
  24. package/src/react-native-core/ReactNativeSessionProvider.ts +29 -3
  25. package/src/react-native-core/tests/ReactNativeSessionProvider.test.ts +175 -3
  26. package/src/tools/auth/clerk/index.ts +21 -11
  27. package/src/tools/auth/clerk/tests/JazzClerkAuth.test.ts +77 -0
  28. package/src/tools/auth/clerk/tests/isClerkAuthStateEqual.test.ts +124 -0
  29. package/src/tools/auth/clerk/types.ts +23 -6
  30. package/src/tools/coValues/CoValueBase.ts +24 -0
  31. package/src/tools/coValues/coMap.ts +1 -1
  32. package/src/tools/implementation/createContext.ts +2 -2
  33. package/src/tools/subscribe/SubscriptionScope.ts +43 -34
  34. package/src/tools/tests/account.test.ts +19 -0
  35. package/src/tools/tests/coMap.record.test.ts +43 -0
  36. package/src/tools/tests/coMap.test.ts +67 -1
  37. package/dist/chunk-GAPMDNJY.js.map +0 -1
@@ -1,9 +1,7 @@
1
- import { LocalNode, RawCoValue } from "cojson";
2
- import { CoList, CoMap, type CoValue, type ID, MaybeLoaded, NotLoaded, type RefEncoded, type RefsToResolve } from "../internal.js";
3
- import { CoValueCoreSubscription } from "./CoValueCoreSubscription.js";
1
+ import { LocalNode } from "cojson";
2
+ import { type CoValue, type ID, MaybeLoaded, NotLoaded, type RefEncoded, type RefsToResolve } from "../internal.js";
4
3
  import { JazzError } from "./JazzError.js";
5
4
  import type { BranchDefinition, SubscriptionValue, SubscriptionValueLoading } from "./types.js";
6
- import { CoValueLoadingState, NotLoadedCoValueState } from "./types.js";
7
5
  import { PromiseWithStatus } from "./utils.js";
8
6
  export declare class SubscriptionScope<D extends CoValue> {
9
7
  node: LocalNode;
@@ -21,24 +19,24 @@ export declare class SubscriptionScope<D extends CoValue> {
21
19
  /**
22
20
  * Autoloaded child ids that are unloaded
23
21
  */
24
- pendingAutoloadedChildren: Set<string>;
22
+ private pendingAutoloadedChildren;
25
23
  value: SubscriptionValue<D, any> | SubscriptionValueLoading;
26
- childErrors: Map<string, JazzError>;
27
- validationErrors: Map<string, JazzError>;
24
+ private childErrors;
25
+ private validationErrors;
28
26
  errorFromChildren: JazzError | undefined;
29
- subscription: CoValueCoreSubscription;
30
- dirty: boolean;
31
- resolve: RefsToResolve<any>;
32
- idsSubscribed: Set<string>;
33
- autoloaded: Set<string>;
34
- autoloadedKeys: Set<string>;
35
- skipInvalidKeys: Set<string>;
36
- totalValidTransactions: number;
37
- version: number;
38
- migrated: boolean;
39
- migrating: boolean;
27
+ private subscription;
28
+ private dirty;
29
+ private resolve;
30
+ private idsSubscribed;
31
+ private autoloaded;
32
+ private autoloadedKeys;
33
+ private skipInvalidKeys;
34
+ private totalValidTransactions;
35
+ private version;
36
+ private migrated;
37
+ private migrating;
40
38
  closed: boolean;
41
- silenceUpdates: boolean;
39
+ private silenceUpdates;
42
40
  /**
43
41
  * Stack trace captured at subscription creation time.
44
42
  * This helps identify which component/hook created the subscription
@@ -47,22 +45,22 @@ export declare class SubscriptionScope<D extends CoValue> {
47
45
  callerStack: Error | undefined;
48
46
  constructor(node: LocalNode, resolve: RefsToResolve<D>, id: ID<D>, schema: RefEncoded<D>, skipRetry?: boolean, bestEffortResolution?: boolean, unstable_branch?: BranchDefinition | undefined, callerStack?: Error | undefined);
49
47
  updateValue(value: SubscriptionValue<D, any>): void;
50
- handleUpdate(update: RawCoValue | typeof CoValueLoadingState.UNAVAILABLE): void;
51
- computeChildErrors(): JazzError | undefined;
52
- handleChildUpdate: (id: string, value: SubscriptionValue<any, any> | SubscriptionValueLoading, key?: string) => void;
53
- shouldSendUpdates(): boolean;
48
+ private handleUpdate;
49
+ private computeChildErrors;
50
+ handleChildUpdate(id: string, value: SubscriptionValue<any, any> | SubscriptionValueLoading, key?: string): void;
51
+ private shouldSendUpdates;
54
52
  unloadedValue: NotLoaded<D> | undefined;
55
- lastPromise: PromiseWithStatus<D> | undefined;
56
- getPromise(): PromiseWithStatus<D>;
53
+ private lastPromise;
54
+ private getPromise;
57
55
  getCachedPromise(): PromiseWithStatus<D>;
58
56
  private getUnloadedValue;
59
- lastErrorLogged: JazzError | undefined;
57
+ private lastErrorLogged;
60
58
  getCurrentValue(): MaybeLoaded<D>;
61
- getCurrentRawValue(): D | NotLoadedCoValueState;
62
- getCreationStackLines(): string;
63
- getError(): JazzError | undefined;
64
- logError(): void;
65
- triggerUpdate(): void;
59
+ private getCurrentRawValue;
60
+ private getCreationStackLines;
61
+ private getError;
62
+ private logError;
63
+ private triggerUpdate;
66
64
  subscribers: Set<(value: SubscriptionValue<D, any>) => void>;
67
65
  subscriberChangeCallbacks: Set<(count: number) => void>;
68
66
  /**
@@ -83,10 +81,10 @@ export declare class SubscriptionScope<D extends CoValue> {
83
81
  */
84
82
  pullValue(listener: (value: SubscriptionValue<D, any>) => void): void;
85
83
  subscribeToId(id: string, descriptor: RefEncoded<any>): void;
86
- loadChildren(): boolean;
87
- loadCoMapKey(map: CoMap, key: string, depth: Record<string, any> | true): string | undefined;
88
- loadCoListKey(list: CoList, key: string, depth: Record<string, any> | true): string | undefined;
89
- loadChildNode(id: string, query: RefsToResolve<any>, descriptor: RefEncoded<any>, key?: string): void;
84
+ private loadChildren;
85
+ private loadCoMapKey;
86
+ private loadCoListKey;
87
+ private loadChildNode;
90
88
  destroy(): void;
91
89
  }
92
90
  //# sourceMappingURL=SubscriptionScope.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"SubscriptionScope.d.ts","sourceRoot":"","sources":["../../../src/tools/subscribe/SubscriptionScope.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAC;AAC/C,OAAO,EAEL,MAAM,EACN,KAAK,EACL,KAAK,OAAO,EACZ,KAAK,EAAE,EACP,WAAW,EACX,SAAS,EACT,KAAK,UAAU,EACf,KAAK,aAAa,EAKnB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,SAAS,EAAuB,MAAM,gBAAgB,CAAC;AAChE,OAAO,KAAK,EACV,gBAAgB,EAChB,iBAAiB,EACjB,wBAAwB,EACzB,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAKxE,OAAO,EAIL,iBAAiB,EAGlB,MAAM,YAAY,CAAC;AAEpB,qBAAa,iBAAiB,CAAC,CAAC,SAAS,OAAO;IAyCrC,IAAI,EAAE,SAAS;IAEf,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC;IACT,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;IACrB,SAAS;IACT,oBAAoB;IACpB,eAAe,CAAC,EAAE,gBAAgB;IA9C3C,UAAU,0CAAiD;IAC3D,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAGjD;IACJ;;OAEG;IACH,qBAAqB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAa;IAC/C;;OAEG;IACH,yBAAyB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAa;IACnD,KAAK,EAAE,iBAAiB,CAAC,CAAC,EAAE,GAAG,CAAC,GAAG,wBAAwB,CAAC;IAC5D,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAa;IAChD,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,CAAC,CAAa;IACrD,iBAAiB,EAAE,SAAS,GAAG,SAAS,CAAC;IACzC,YAAY,EAAE,uBAAuB,CAAC;IACtC,KAAK,UAAS;IACd,OAAO,EAAE,aAAa,CAAC,GAAG,CAAC,CAAC;IAC5B,aAAa,cAAqB;IAClC,UAAU,cAAqB;IAC/B,cAAc,cAAqB;IACnC,eAAe,cAAqB;IACpC,sBAAsB,SAAK;IAC3B,OAAO,SAAK;IACZ,QAAQ,UAAS;IACjB,SAAS,UAAS;IAClB,MAAM,UAAS;IAEf,cAAc,UAAS;IAEvB;;;;OAIG;IACH,WAAW,EAAE,KAAK,GAAG,SAAS,CAAC;gBAGtB,IAAI,EAAE,SAAS,EACtB,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC,EAClB,EAAE,EAAE,EAAE,CAAC,CAAC,CAAC,EACT,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC,EACrB,SAAS,UAAQ,EACjB,oBAAoB,UAAQ,EAC5B,eAAe,CAAC,EAAE,gBAAgB,YAAA,EACzC,WAAW,CAAC,EAAE,KAAK,GAAG,SAAS;IAsDjC,WAAW,CAAC,KAAK,EAAE,iBAAiB,CAAC,CAAC,EAAE,GAAG,CAAC;IAO5C,YAAY,CAAC,MAAM,EAAE,UAAU,GAAG,OAAO,mBAAmB,CAAC,WAAW;IAoExE,kBAAkB;IA8ClB,iBAAiB,OACX,MAAM,SACH,iBAAiB,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,wBAAwB,QACvD,MAAM,UAkCZ;IAEF,iBAAiB;IASjB,aAAa,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;IAExC,WAAW,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC;IAE9C,UAAU;IAoDV,gBAAgB;IA4BhB,OAAO,CAAC,gBAAgB;IAYxB,eAAe,EAAE,SAAS,GAAG,SAAS,CAAC;IAEvC,eAAe,IAAI,WAAW,CAAC,CAAC,CAAC;IAejC,kBAAkB,IAAI,CAAC,GAAG,qBAAqB;IAuB/C,qBAAqB;IA+BrB,QAAQ;IAaR,QAAQ;IAuBR,aAAa;IAkBb,WAAW,cAAmB,iBAAiB,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK,IAAI,EAAI;IACpE,yBAAyB,cAAmB,MAAM,KAAK,IAAI,EAAI;IAE/D;;;;OAIG;IACH,kBAAkB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI;IAQjE,OAAO,CAAC,sBAAsB;IAO9B,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK,IAAI;IAU9D,WAAW,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK,IAAI;IAUhE,cAAc,CAAC,GAAG,EAAE,MAAM;IAsC1B,gBAAgB,CAAC,EAAE,EAAE,MAAM;IAS3B;;;;OAIG;IACH,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,CAAC,CAAC,EAAE,GAAG,CAAC,KAAK,IAAI;IA0B9D,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,CAAC,GAAG,CAAC;IAmDrD,YAAY;IAoHZ,YAAY,CAAC,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI;IA0CvE,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI;IAsC1E,aAAa,CACX,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,aAAa,CAAC,GAAG,CAAC,EACzB,UAAU,EAAE,UAAU,CAAC,GAAG,CAAC,EAC3B,GAAG,CAAC,EAAE,MAAM;IAoDd,OAAO;CAcR"}
1
+ {"version":3,"file":"SubscriptionScope.d.ts","sourceRoot":"","sources":["../../../src/tools/subscribe/SubscriptionScope.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -205,7 +205,7 @@
205
205
  },
206
206
  "type": "module",
207
207
  "license": "MIT",
208
- "version": "0.19.14",
208
+ "version": "0.19.16",
209
209
  "dependencies": {
210
210
  "@manuscripts/prosemirror-recreate-steps": "^0.1.4",
211
211
  "@scure/base": "1.2.1",
@@ -222,9 +222,9 @@
222
222
  "prosemirror-transform": "^1.9.0",
223
223
  "use-sync-external-store": "^1.5.0",
224
224
  "zod": "4.1.11",
225
- "cojson": "0.19.14",
226
- "cojson-storage-indexeddb": "0.19.14",
227
- "cojson-transport-ws": "0.19.14"
225
+ "cojson": "0.19.16",
226
+ "cojson-storage-indexeddb": "0.19.16",
227
+ "cojson-transport-ws": "0.19.16"
228
228
  },
229
229
  "devDependencies": {
230
230
  "@scure/bip39": "^1.3.0",
@@ -6,6 +6,8 @@ import {
6
6
  } from "jazz-tools";
7
7
  import { AgentID, RawAccountID } from "cojson";
8
8
 
9
+ const lockedSessions = new Set<SessionID>();
10
+
9
11
  export class ReactNativeSessionProvider implements SessionProvider {
10
12
  async acquireSession(
11
13
  accountID: string,
@@ -14,11 +16,29 @@ export class ReactNativeSessionProvider implements SessionProvider {
14
16
  const kvStore = KvStoreContext.getInstance().getStorage();
15
17
  const existingSession = await kvStore.get(accountID as string);
16
18
 
19
+ // Check if the session is already in use, should happen only if the dev
20
+ // mounts multiple providers at the same time
21
+ if (lockedSessions.has(existingSession as SessionID)) {
22
+ const newSessionID = crypto.newRandomSessionID(
23
+ accountID as RawAccountID | AgentID,
24
+ );
25
+
26
+ console.error("Existing session in use, creating new one", newSessionID);
27
+
28
+ return Promise.resolve({
29
+ sessionID: newSessionID,
30
+ sessionDone: () => {},
31
+ });
32
+ }
33
+
17
34
  if (existingSession) {
18
35
  console.log("Using existing session", existingSession);
36
+ lockedSessions.add(existingSession as SessionID);
19
37
  return Promise.resolve({
20
38
  sessionID: existingSession as SessionID,
21
- sessionDone: () => {},
39
+ sessionDone: () => {
40
+ lockedSessions.delete(existingSession as SessionID);
41
+ },
22
42
  });
23
43
  }
24
44
 
@@ -30,12 +50,15 @@ export class ReactNativeSessionProvider implements SessionProvider {
30
50
  accountID as RawAccountID | AgentID,
31
51
  );
32
52
  await kvStore.set(accountID, newSessionID);
53
+ lockedSessions.add(newSessionID);
33
54
 
34
55
  console.error("Created new session", newSessionID);
35
56
 
36
57
  return Promise.resolve({
37
58
  sessionID: newSessionID,
38
- sessionDone: () => {},
59
+ sessionDone: () => {
60
+ lockedSessions.delete(newSessionID);
61
+ },
39
62
  });
40
63
  }
41
64
 
@@ -45,8 +68,11 @@ export class ReactNativeSessionProvider implements SessionProvider {
45
68
  ): Promise<{ sessionDone: () => void }> {
46
69
  const kvStore = KvStoreContext.getInstance().getStorage();
47
70
  await kvStore.set(accountID, sessionID);
71
+ lockedSessions.add(sessionID);
48
72
  return Promise.resolve({
49
- sessionDone: () => {},
73
+ sessionDone: () => {
74
+ lockedSessions.delete(sessionID);
75
+ },
50
76
  });
51
77
  }
52
78
  }
@@ -1,5 +1,5 @@
1
1
  import { WasmCrypto } from "cojson/crypto/WasmCrypto";
2
- import { SessionID } from "cojson";
2
+ import { RawAccountID, SessionID } from "cojson";
3
3
  import { beforeEach, describe, expect, test } from "vitest";
4
4
  import { InMemoryKVStore } from "jazz-tools";
5
5
  import { KvStoreContext, type KvStore } from "jazz-tools";
@@ -51,6 +51,9 @@ describe("ReactNativeSessionProvider", () => {
51
51
  const storedSession = await kvStore.get(accountID);
52
52
  expect(storedSession).toBeDefined();
53
53
  expect(storedSession).toBe(result.sessionID);
54
+
55
+ // Clean up
56
+ result.sessionDone();
54
57
  });
55
58
 
56
59
  test("returns existing session when one exists", async () => {
@@ -77,6 +80,89 @@ describe("ReactNativeSessionProvider", () => {
77
80
  const sessionAfter = await kvStore.get(accountID);
78
81
  expect(sessionAfter).toBe(existingSessionID);
79
82
  expect(sessionAfter).toBe(result.sessionID);
83
+
84
+ // Clean up
85
+ result.sessionDone();
86
+ });
87
+
88
+ test("creates new session when existing session is locked", async () => {
89
+ const accountID = account.$jazz.id;
90
+ const existingSessionID = Crypto.newRandomSessionID(
91
+ accountID as RawAccountID,
92
+ );
93
+
94
+ // Pre-populate KvStore with a session ID
95
+ await kvStore.set(accountID, existingSessionID);
96
+
97
+ // Acquire the session (this locks it)
98
+ const firstResult = await sessionProvider.acquireSession(
99
+ accountID,
100
+ Crypto as CryptoProvider,
101
+ );
102
+ expect(firstResult.sessionID).toBe(existingSessionID);
103
+
104
+ // Try to acquire session again while the first is still locked
105
+ const secondResult = await sessionProvider.acquireSession(
106
+ accountID,
107
+ Crypto as CryptoProvider,
108
+ );
109
+
110
+ // Should get a different (new) session since the existing one is locked
111
+ expect(secondResult.sessionID).not.toBe(existingSessionID);
112
+ expect(secondResult.sessionID).toBeDefined();
113
+
114
+ // Clean up
115
+ firstResult.sessionDone();
116
+ secondResult.sessionDone();
117
+ });
118
+
119
+ test("reuses session after sessionDone is called", async () => {
120
+ const accountID = account.$jazz.id;
121
+
122
+ // Acquire initial session
123
+ const firstResult = await sessionProvider.acquireSession(
124
+ accountID,
125
+ Crypto as CryptoProvider,
126
+ );
127
+ const firstSessionID = firstResult.sessionID;
128
+
129
+ // Release the session
130
+ firstResult.sessionDone();
131
+
132
+ // Acquire session again - should reuse the same session
133
+ const secondResult = await sessionProvider.acquireSession(
134
+ accountID,
135
+ Crypto as CryptoProvider,
136
+ );
137
+
138
+ expect(secondResult.sessionID).toBe(firstSessionID);
139
+
140
+ // Clean up
141
+ secondResult.sessionDone();
142
+ });
143
+
144
+ test("sessionDone can be called multiple times safely", async () => {
145
+ const accountID = account.$jazz.id;
146
+
147
+ const result = await sessionProvider.acquireSession(
148
+ accountID,
149
+ Crypto as CryptoProvider,
150
+ );
151
+
152
+ // Call sessionDone multiple times - should not throw
153
+ result.sessionDone();
154
+ result.sessionDone();
155
+ result.sessionDone();
156
+
157
+ // Should still be able to acquire the session
158
+ const secondResult = await sessionProvider.acquireSession(
159
+ accountID,
160
+ Crypto as CryptoProvider,
161
+ );
162
+ expect(secondResult.sessionID).toBe(result.sessionID);
163
+
164
+ // Clean up
165
+ secondResult.sessionDone();
80
166
  });
81
167
  });
82
168
 
@@ -90,7 +176,10 @@ describe("ReactNativeSessionProvider", () => {
90
176
  expect(sessionBefore).toBeNull();
91
177
 
92
178
  // Persist session
93
- await sessionProvider.persistSession(accountID, sessionID);
179
+ const { sessionDone } = await sessionProvider.persistSession(
180
+ accountID,
181
+ sessionID,
182
+ );
94
183
 
95
184
  // Verify the session ID is stored in KvStore
96
185
  const storedSession = await kvStore.get(accountID);
@@ -98,6 +187,9 @@ describe("ReactNativeSessionProvider", () => {
98
187
 
99
188
  // Verify the stored value matches the provided session ID
100
189
  expect(storedSession).toBe(sessionID);
190
+
191
+ // Clean up
192
+ sessionDone();
101
193
  });
102
194
 
103
195
  test("overwrites existing session", async () => {
@@ -113,12 +205,92 @@ describe("ReactNativeSessionProvider", () => {
113
205
  expect(sessionBefore).toBe(initialSessionID);
114
206
 
115
207
  // Persist a different session ID
116
- await sessionProvider.persistSession(accountID, newSessionID);
208
+ const { sessionDone } = await sessionProvider.persistSession(
209
+ accountID,
210
+ newSessionID,
211
+ );
117
212
 
118
213
  // Verify the new session ID replaces the old one
119
214
  const sessionAfter = await kvStore.get(accountID);
120
215
  expect(sessionAfter).toBe(newSessionID);
121
216
  expect(sessionAfter).not.toBe(initialSessionID);
217
+
218
+ // Clean up
219
+ sessionDone();
220
+ });
221
+
222
+ test("locks session when persisting", async () => {
223
+ const accountID = account.$jazz.id;
224
+ const sessionID = Crypto.newRandomSessionID(accountID as RawAccountID);
225
+
226
+ // Persist session - this should lock the session
227
+ const { sessionDone } = await sessionProvider.persistSession(
228
+ accountID,
229
+ sessionID,
230
+ );
231
+
232
+ // Try to acquire session while it's locked by persistSession
233
+ const result = await sessionProvider.acquireSession(
234
+ accountID,
235
+ Crypto as CryptoProvider,
236
+ );
237
+
238
+ // Should get a different session since the persisted one is locked
239
+ expect(result.sessionID).not.toBe(sessionID);
240
+
241
+ // Clean up
242
+ sessionDone();
243
+ result.sessionDone();
244
+ });
245
+
246
+ test("allows session reuse after sessionDone is called", async () => {
247
+ const accountID = account.$jazz.id;
248
+ const sessionID = Crypto.newRandomSessionID(accountID as RawAccountID);
249
+
250
+ // Persist session
251
+ const { sessionDone } = await sessionProvider.persistSession(
252
+ accountID,
253
+ sessionID,
254
+ );
255
+
256
+ // Release the session
257
+ sessionDone();
258
+
259
+ // Acquire session - should reuse the persisted session
260
+ const result = await sessionProvider.acquireSession(
261
+ accountID,
262
+ Crypto as CryptoProvider,
263
+ );
264
+
265
+ expect(result.sessionID).toBe(sessionID);
266
+
267
+ // Clean up
268
+ result.sessionDone();
269
+ });
270
+
271
+ test("sessionDone can be called multiple times safely", async () => {
272
+ const accountID = account.$jazz.id;
273
+ const sessionID = Crypto.newRandomSessionID(accountID as RawAccountID);
274
+
275
+ const { sessionDone } = await sessionProvider.persistSession(
276
+ accountID,
277
+ sessionID,
278
+ );
279
+
280
+ // Call sessionDone multiple times - should not throw
281
+ sessionDone();
282
+ sessionDone();
283
+ sessionDone();
284
+
285
+ // Should still be able to acquire the session
286
+ const result = await sessionProvider.acquireSession(
287
+ accountID,
288
+ Crypto as CryptoProvider,
289
+ );
290
+ expect(result.sessionID).toBe(sessionID);
291
+
292
+ // Clean up
293
+ result.sessionDone();
122
294
  });
123
295
  });
124
296
  });
@@ -53,18 +53,21 @@ export class JazzClerkAuth {
53
53
  }
54
54
 
55
55
  private isFirstCall = true;
56
+ private previousUser: Pick<
57
+ NonNullable<MinimalClerkClient["user"]>,
58
+ "unsafeMetadata"
59
+ > | null = null;
56
60
 
57
61
  registerListener(clerkClient: MinimalClerkClient) {
58
- let previousUser: MinimalClerkClient["user"] | null =
59
- clerkClient.user ?? null;
62
+ this.previousUser = clerkClient.user ?? null;
60
63
 
61
64
  // Need to use addListener because the clerk user object is not updated when the user logs in
62
65
  return clerkClient.addListener((event) => {
63
66
  const user = (event as Pick<MinimalClerkClient, "user">).user ?? null;
64
67
 
65
- if (!isClerkAuthStateEqual(previousUser, user) || this.isFirstCall) {
68
+ if (!isClerkAuthStateEqual(this.previousUser, user) || this.isFirstCall) {
69
+ this.previousUser = user;
66
70
  this.onClerkUserChange({ user });
67
- previousUser = user;
68
71
  this.isFirstCall = false;
69
72
  }
70
73
  });
@@ -137,13 +140,20 @@ export class JazzClerkAuth {
137
140
  ? Array.from(credentials.secretSeed)
138
141
  : undefined;
139
142
 
140
- await clerkClient.user?.update({
141
- unsafeMetadata: {
142
- jazzAccountID: credentials.accountID,
143
- jazzAccountSecret: credentials.accountSecret,
144
- jazzAccountSeed,
145
- } satisfies ClerkCredentials,
146
- });
143
+ const clerkCredentials = {
144
+ jazzAccountID: credentials.accountID,
145
+ jazzAccountSecret: credentials.accountSecret,
146
+ jazzAccountSeed,
147
+ };
148
+ // user.update will cause the Clerk user change listener to fire; updating this.previousUser beforehand
149
+ // ensures the listener sees the new credentials and does not trigger an unnecessary logIn operation
150
+ this.previousUser = { unsafeMetadata: clerkCredentials };
151
+
152
+ if (clerkClient.user) {
153
+ await clerkClient.user.update({
154
+ unsafeMetadata: clerkCredentials,
155
+ });
156
+ }
147
157
 
148
158
  const currentAccount = await Account.getMe().$jazz.ensureLoaded({
149
159
  resolve: {
@@ -282,6 +282,83 @@ describe("JazzClerkAuth", () => {
282
282
 
283
283
  expect(onClerkUserChangeSpy).toHaveBeenCalledTimes(1);
284
284
  });
285
+
286
+ it("should complete signup flow when new Clerk user is detected", async () => {
287
+ // 1. Setup local credentials (simulating anonymous user)
288
+ await authSecretStorage.set({
289
+ accountID: "test-account-id" as ID<Account>,
290
+ secretSeed: new Uint8Array([1, 2, 3]),
291
+ accountSecret: "test-secret" as AgentSecret,
292
+ provider: "anonymous",
293
+ });
294
+
295
+ const { client, triggerUserChange } = setupMockClerk(null);
296
+
297
+ const auth = new JazzClerkAuth(
298
+ mockAuthenticate,
299
+ mockLogOut,
300
+ authSecretStorage,
301
+ );
302
+
303
+ // 2. Register listener with null user (no one logged in yet)
304
+ auth.registerListener(client);
305
+
306
+ // Initial trigger with no user
307
+ triggerUserChange(null);
308
+
309
+ // 3. Trigger event with new Clerk user (no Jazz credentials yet)
310
+ const mockUserUpdate = vi.fn((data) => {
311
+ triggerUserChange(data);
312
+ });
313
+
314
+ const signInSpy = vi.spyOn(auth, "signIn");
315
+ const logInSpy = vi.spyOn(auth, "logIn");
316
+
317
+ const newClerkUser = {
318
+ fullName: "Test User",
319
+ firstName: "Test",
320
+ lastName: "User",
321
+ username: "testuser",
322
+ id: "clerk-user-123",
323
+ primaryEmailAddress: { emailAddress: "test@example.com" },
324
+ unsafeMetadata: {}, // No Jazz credentials yet
325
+ update: mockUserUpdate,
326
+ };
327
+
328
+ triggerUserChange(newClerkUser);
329
+
330
+ // Wait for async operations to complete
331
+ await vi.waitFor(() => {
332
+ expect(mockUserUpdate).toHaveBeenCalled();
333
+ });
334
+
335
+ // 4. Verify credentials synced to Clerk
336
+ expect(mockUserUpdate).toHaveBeenCalledWith({
337
+ unsafeMetadata: {
338
+ jazzAccountID: "test-account-id",
339
+ jazzAccountSecret: "test-secret",
340
+ jazzAccountSeed: [1, 2, 3],
341
+ },
342
+ });
343
+
344
+ // Verify profile name was updated from Clerk username
345
+ const me = await Account.getMe().$jazz.ensureLoaded({
346
+ resolve: { profile: true },
347
+ });
348
+ expect(me.profile.name).toBe("Test User");
349
+
350
+ // Verify authSecretStorage is updated with provider "clerk"
351
+ const storedCredentials = await authSecretStorage.get();
352
+ expect(storedCredentials).toEqual({
353
+ accountID: "test-account-id",
354
+ accountSecret: "test-secret",
355
+ secretSeed: new Uint8Array([1, 2, 3]),
356
+ provider: "clerk",
357
+ });
358
+
359
+ expect(signInSpy).toHaveBeenCalled();
360
+ expect(logInSpy).not.toHaveBeenCalled();
361
+ });
285
362
  });
286
363
 
287
364
  describe("initializeAuth", () => {