jazz-tools 0.19.11 → 0.19.13
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/.turbo/turbo-build.log +51 -51
- package/CHANGELOG.md +22 -0
- package/dist/browser/createBrowserContext.d.ts +1 -5
- package/dist/browser/createBrowserContext.d.ts.map +1 -1
- package/dist/browser/index.js +124 -47
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/provideBrowserLockSession/BrowserSessionProvider.d.ts +12 -0
- package/dist/browser/provideBrowserLockSession/BrowserSessionProvider.d.ts.map +1 -0
- package/dist/browser/provideBrowserLockSession/BrowserSessionProvider.test.d.ts +2 -0
- package/dist/browser/provideBrowserLockSession/BrowserSessionProvider.test.d.ts.map +1 -0
- package/dist/browser/provideBrowserLockSession/SessionIDStorage.d.ts +6 -0
- package/dist/browser/provideBrowserLockSession/SessionIDStorage.d.ts.map +1 -0
- package/dist/browser/provideBrowserLockSession/index.d.ts +4 -0
- package/dist/browser/provideBrowserLockSession/index.d.ts.map +1 -0
- package/dist/{chunk-HX5S6W5E.js → chunk-GAPMDNJY.js} +492 -108
- package/dist/chunk-GAPMDNJY.js.map +1 -0
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/inspector/{chunk-C6BJPHBQ.js → chunk-YQNK5Y7B.js} +47 -35
- package/dist/inspector/chunk-YQNK5Y7B.js.map +1 -0
- package/dist/inspector/{custom-element-GJVBPZES.js → custom-element-KYV64IOC.js} +47 -35
- package/dist/inspector/{custom-element-GJVBPZES.js.map → custom-element-KYV64IOC.js.map} +1 -1
- package/dist/inspector/index.js +1 -1
- package/dist/inspector/register-custom-element.js +1 -1
- package/dist/inspector/standalone.js +1 -1
- package/dist/inspector/tests/utils/transactions-changes.test.d.ts +2 -0
- package/dist/inspector/tests/utils/transactions-changes.test.d.ts.map +1 -0
- package/dist/inspector/utils/transactions-changes.d.ts +13 -13
- package/dist/inspector/utils/transactions-changes.d.ts.map +1 -1
- package/dist/react/index.js +4 -1
- package/dist/react/index.js.map +1 -1
- package/dist/react/provider.d.ts.map +1 -1
- package/dist/react-core/index.js +2 -2
- package/dist/react-core/index.js.map +1 -1
- package/dist/react-native/index.js +45 -13
- package/dist/react-native/index.js.map +1 -1
- package/dist/react-native-core/ReactNativeSessionProvider.d.ts +11 -0
- package/dist/react-native-core/ReactNativeSessionProvider.d.ts.map +1 -0
- package/dist/react-native-core/index.js +45 -13
- package/dist/react-native-core/index.js.map +1 -1
- package/dist/react-native-core/platform.d.ts +2 -8
- package/dist/react-native-core/platform.d.ts.map +1 -1
- package/dist/react-native-core/provider.d.ts.map +1 -1
- package/dist/react-native-core/tests/ReactNativeSessionProvider.test.d.ts +2 -0
- package/dist/react-native-core/tests/ReactNativeSessionProvider.test.d.ts.map +1 -0
- package/dist/testing.js +4 -3
- package/dist/testing.js.map +1 -1
- package/dist/tools/coValues/account.d.ts.map +1 -1
- package/dist/tools/coValues/coFeed.d.ts +2 -2
- package/dist/tools/coValues/coFeed.d.ts.map +1 -1
- package/dist/tools/coValues/coList.d.ts +1 -2
- package/dist/tools/coValues/coList.d.ts.map +1 -1
- package/dist/tools/coValues/coMap.d.ts.map +1 -1
- package/dist/tools/coValues/coVector.d.ts.map +1 -1
- package/dist/tools/coValues/group.d.ts +5 -1
- package/dist/tools/coValues/group.d.ts.map +1 -1
- package/dist/tools/coValues/interfaces.d.ts +2 -1
- package/dist/tools/coValues/interfaces.d.ts.map +1 -1
- package/dist/tools/exports.d.ts +2 -2
- package/dist/tools/exports.d.ts.map +1 -1
- package/dist/tools/implementation/ContextManager.d.ts.map +1 -1
- package/dist/tools/implementation/createContext.d.ts +21 -11
- package/dist/tools/implementation/createContext.d.ts.map +1 -1
- package/dist/tools/implementation/schema.d.ts +14 -6
- package/dist/tools/implementation/schema.d.ts.map +1 -1
- package/dist/tools/implementation/schemaUtils.d.ts +1 -1
- package/dist/tools/implementation/schemaUtils.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/runtimeConverters/schemaFieldToCoFieldDef.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaPermissions.d.ts +99 -0
- package/dist/tools/implementation/zodSchema/schemaPermissions.d.ts.map +1 -0
- package/dist/tools/implementation/zodSchema/schemaTypes/CoFeedSchema.d.ts +11 -0
- package/dist/tools/implementation/zodSchema/schemaTypes/CoFeedSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/CoListSchema.d.ts +11 -0
- package/dist/tools/implementation/zodSchema/schemaTypes/CoListSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts +15 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/CoMapSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/CoRecordSchema.d.ts +10 -0
- package/dist/tools/implementation/zodSchema/schemaTypes/CoRecordSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/CoVectorSchema.d.ts +9 -0
- package/dist/tools/implementation/zodSchema/schemaTypes/CoVectorSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.d.ts +13 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/PlainTextSchema.d.ts +10 -0
- package/dist/tools/implementation/zodSchema/schemaTypes/PlainTextSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/schemaTypes/RichTextSchema.d.ts +6 -0
- package/dist/tools/implementation/zodSchema/schemaTypes/RichTextSchema.d.ts.map +1 -1
- package/dist/tools/implementation/zodSchema/unionUtils.d.ts +12 -1
- package/dist/tools/implementation/zodSchema/unionUtils.d.ts.map +1 -1
- package/dist/tools/internal.d.ts +1 -0
- package/dist/tools/internal.d.ts.map +1 -1
- package/dist/tools/subscribe/SubscriptionScope.d.ts +3 -6
- package/dist/tools/subscribe/SubscriptionScope.d.ts.map +1 -1
- package/dist/tools/testing.d.ts.map +1 -1
- package/dist/tools/tests/schema.withPermissions.test.d.ts +2 -0
- package/dist/tools/tests/schema.withPermissions.test.d.ts.map +1 -0
- package/dist/worker/index.js +2 -2
- package/dist/worker/index.js.map +1 -1
- package/package.json +4 -4
- package/src/browser/createBrowserContext.ts +3 -62
- package/src/browser/provideBrowserLockSession/BrowserSessionProvider.test.ts +406 -0
- package/src/browser/provideBrowserLockSession/BrowserSessionProvider.ts +132 -0
- package/src/browser/provideBrowserLockSession/SessionIDStorage.ts +33 -0
- package/src/browser/provideBrowserLockSession/index.ts +11 -0
- package/src/inspector/tests/utils/transactions-changes.test.ts +102 -0
- package/src/inspector/ui/icons/add-icon.tsx +3 -3
- package/src/inspector/utils/history.ts +6 -6
- package/src/inspector/utils/transactions-changes.ts +37 -3
- package/src/inspector/viewer/history-view.tsx +13 -13
- package/src/react/provider.tsx +6 -1
- package/src/react-core/hooks.ts +2 -2
- package/src/react-core/tests/useSuspenseCoState.test.tsx +47 -0
- package/src/react-native-core/ReactNativeSessionProvider.ts +52 -0
- package/src/react-native-core/platform.ts +5 -30
- package/src/react-native-core/provider.tsx +6 -1
- package/src/react-native-core/tests/ReactNativeSessionProvider.test.ts +124 -0
- package/src/tools/coValues/account.ts +4 -0
- package/src/tools/coValues/coFeed.ts +8 -3
- package/src/tools/coValues/coList.ts +6 -3
- package/src/tools/coValues/coMap.ts +10 -0
- package/src/tools/coValues/coVector.ts +2 -1
- package/src/tools/coValues/group.ts +6 -4
- package/src/tools/coValues/interfaces.ts +19 -7
- package/src/tools/exports.ts +3 -1
- package/src/tools/implementation/ContextManager.ts +10 -0
- package/src/tools/implementation/createContext.ts +43 -15
- package/src/tools/implementation/schema.ts +23 -13
- package/src/tools/implementation/schemaUtils.ts +1 -1
- package/src/tools/implementation/zodSchema/runtimeConverters/schemaFieldToCoFieldDef.ts +105 -4
- package/src/tools/implementation/zodSchema/schemaPermissions.ts +188 -0
- package/src/tools/implementation/zodSchema/schemaTypes/CoFeedSchema.ts +46 -3
- package/src/tools/implementation/zodSchema/schemaTypes/CoListSchema.ts +46 -3
- package/src/tools/implementation/zodSchema/schemaTypes/CoMapSchema.ts +50 -13
- package/src/tools/implementation/zodSchema/schemaTypes/CoRecordSchema.ts +14 -0
- package/src/tools/implementation/zodSchema/schemaTypes/CoVectorSchema.ts +24 -1
- package/src/tools/implementation/zodSchema/schemaTypes/FileStreamSchema.ts +51 -4
- package/src/tools/implementation/zodSchema/schemaTypes/PlainTextSchema.ts +25 -1
- package/src/tools/implementation/zodSchema/schemaTypes/RichTextSchema.ts +21 -1
- package/src/tools/implementation/zodSchema/unionUtils.ts +72 -20
- package/src/tools/internal.ts +1 -0
- package/src/tools/subscribe/SubscriptionScope.ts +61 -39
- package/src/tools/testing.ts +3 -1
- package/src/tools/tests/ContextManager.test.ts +2 -1
- package/src/tools/tests/coPlainText.test.ts +2 -2
- package/src/tools/tests/createContext.test.ts +79 -1
- package/src/tools/tests/deepLoading.test.ts +25 -2
- package/src/tools/tests/schema.resolved.test.ts +10 -0
- package/src/tools/tests/schema.withPermissions.test.ts +859 -0
- package/src/tools/tests/utils.ts +2 -2
- package/src/worker/index.ts +2 -2
- package/dist/chunk-HX5S6W5E.js.map +0 -1
- package/dist/inspector/chunk-C6BJPHBQ.js.map +0 -1
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { WasmCrypto } from "cojson/crypto/WasmCrypto";
|
|
4
|
+
import { SessionID } from "cojson";
|
|
5
|
+
import { beforeEach, describe, expect, test, vi } from "vitest";
|
|
6
|
+
import { BrowserSessionProvider } from "./BrowserSessionProvider.js";
|
|
7
|
+
import { SessionIDStorage } from "./SessionIDStorage.js";
|
|
8
|
+
import { createJazzTestAccount } from "jazz-tools/testing";
|
|
9
|
+
import type { CryptoProvider } from "jazz-tools";
|
|
10
|
+
|
|
11
|
+
const Crypto = await WasmCrypto.create();
|
|
12
|
+
|
|
13
|
+
// Mock navigator.locks
|
|
14
|
+
interface LockInfo {
|
|
15
|
+
mode: "exclusive" | "shared";
|
|
16
|
+
release: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const mockLocks = new Map<string, LockInfo>();
|
|
20
|
+
|
|
21
|
+
function isLockAvailable(
|
|
22
|
+
name: string,
|
|
23
|
+
requestedMode: "exclusive" | "shared",
|
|
24
|
+
): boolean {
|
|
25
|
+
const existingLock = mockLocks.get(name);
|
|
26
|
+
if (!existingLock) {
|
|
27
|
+
return true; // No existing lock
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Exclusive locks block everything
|
|
31
|
+
if (existingLock.mode === "exclusive" || requestedMode === "exclusive") {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Shared locks can coexist with other shared locks
|
|
36
|
+
if (existingLock.mode === "shared" && requestedMode === "shared") {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function createMockLock(
|
|
44
|
+
name: string,
|
|
45
|
+
mode: "exclusive" | "shared",
|
|
46
|
+
): {
|
|
47
|
+
lock: { name: string } | null;
|
|
48
|
+
release: () => void;
|
|
49
|
+
} {
|
|
50
|
+
if (!isLockAvailable(name, mode)) {
|
|
51
|
+
return { lock: null, release: () => {} };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Create new lock
|
|
55
|
+
const lockInfo: LockInfo = {
|
|
56
|
+
mode,
|
|
57
|
+
release: () => {
|
|
58
|
+
mockLocks.delete(name);
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
mockLocks.set(name, lockInfo);
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
lock: { name },
|
|
66
|
+
release: lockInfo.release,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
vi.stubGlobal("navigator", {
|
|
71
|
+
locks: {
|
|
72
|
+
request: vi.fn(
|
|
73
|
+
async (
|
|
74
|
+
name: string,
|
|
75
|
+
options: { mode?: "exclusive" | "shared"; ifAvailable?: boolean },
|
|
76
|
+
callback: (lock: { name: string } | null) => Promise<void> | void,
|
|
77
|
+
) => {
|
|
78
|
+
const mode = options?.mode || "exclusive";
|
|
79
|
+
const ifAvailable = options?.ifAvailable || false;
|
|
80
|
+
|
|
81
|
+
if (ifAvailable) {
|
|
82
|
+
const { lock } = createMockLock(name, mode);
|
|
83
|
+
if (!lock) {
|
|
84
|
+
// Lock not available, call callback with null and return immediately
|
|
85
|
+
await callback(null);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// Lock available, call callback with lock
|
|
89
|
+
// The lock is held until the promise returned from callback resolves
|
|
90
|
+
const callbackPromise = callback(lock);
|
|
91
|
+
const result = await callbackPromise;
|
|
92
|
+
// Release lock after callback promise resolves
|
|
93
|
+
mockLocks.get(name)?.release();
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// For non-ifAvailable locks, wait until available
|
|
98
|
+
// In a real implementation, this would wait, but for tests we assume immediate availability
|
|
99
|
+
const { lock, release } = createMockLock(name, mode);
|
|
100
|
+
if (!lock) {
|
|
101
|
+
// This shouldn't happen in tests for non-ifAvailable locks
|
|
102
|
+
// But handle it gracefully
|
|
103
|
+
await callback(null);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const callbackPromise = callback(lock);
|
|
109
|
+
const result = await callbackPromise;
|
|
110
|
+
return result;
|
|
111
|
+
} finally {
|
|
112
|
+
release();
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
),
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe("BrowserSessionProvider", () => {
|
|
120
|
+
let sessionProvider: BrowserSessionProvider;
|
|
121
|
+
let account: Awaited<ReturnType<typeof createJazzTestAccount>>;
|
|
122
|
+
|
|
123
|
+
beforeEach(async () => {
|
|
124
|
+
// Clear localStorage
|
|
125
|
+
localStorage.clear();
|
|
126
|
+
|
|
127
|
+
// Clear mock locks
|
|
128
|
+
mockLocks.clear();
|
|
129
|
+
|
|
130
|
+
// Create new session provider instance
|
|
131
|
+
sessionProvider = new BrowserSessionProvider();
|
|
132
|
+
|
|
133
|
+
// Create test account
|
|
134
|
+
account = await createJazzTestAccount({
|
|
135
|
+
isCurrentActiveAccount: true,
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe("acquireSession", () => {
|
|
140
|
+
test("creates new session when none exists", async () => {
|
|
141
|
+
const accountID = account.$jazz.id;
|
|
142
|
+
|
|
143
|
+
// Verify no sessions exist
|
|
144
|
+
const existingSessionsBefore =
|
|
145
|
+
SessionIDStorage.getSessionsList(accountID);
|
|
146
|
+
expect(existingSessionsBefore).toEqual([]);
|
|
147
|
+
|
|
148
|
+
// Acquire session
|
|
149
|
+
const result = await sessionProvider.acquireSession(
|
|
150
|
+
accountID,
|
|
151
|
+
Crypto as CryptoProvider,
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// Verify a new session ID is generated
|
|
155
|
+
expect(result.sessionID).toBeDefined();
|
|
156
|
+
|
|
157
|
+
// Verify the session is stored in localStorage
|
|
158
|
+
const storedSessions = SessionIDStorage.getSessionsList(accountID);
|
|
159
|
+
expect(storedSessions).toHaveLength(1);
|
|
160
|
+
expect(storedSessions[0]).toBe(result.sessionID);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("returns existing session when available", async () => {
|
|
164
|
+
const accountID = account.$jazz.id;
|
|
165
|
+
const existingSessionID = "existing-session-id" as SessionID;
|
|
166
|
+
|
|
167
|
+
// Pre-populate localStorage with a session ID
|
|
168
|
+
SessionIDStorage.storeSessionID(accountID, existingSessionID, 0);
|
|
169
|
+
|
|
170
|
+
// Verify session exists before calling acquireSession
|
|
171
|
+
const sessionsBefore = SessionIDStorage.getSessionsList(accountID);
|
|
172
|
+
expect(sessionsBefore).toHaveLength(1);
|
|
173
|
+
expect(sessionsBefore[0]).toBe(existingSessionID);
|
|
174
|
+
|
|
175
|
+
// Acquire session
|
|
176
|
+
const result = await sessionProvider.acquireSession(
|
|
177
|
+
accountID,
|
|
178
|
+
Crypto as CryptoProvider,
|
|
179
|
+
);
|
|
180
|
+
|
|
181
|
+
// Verify the existing session ID is returned (not a new one)
|
|
182
|
+
expect(result.sessionID).toBe(existingSessionID);
|
|
183
|
+
|
|
184
|
+
// Verify no new session is created (same value still in store)
|
|
185
|
+
const sessionsAfter = SessionIDStorage.getSessionsList(accountID);
|
|
186
|
+
expect(sessionsAfter).toHaveLength(1);
|
|
187
|
+
expect(sessionsAfter[0]).toBe(existingSessionID);
|
|
188
|
+
expect(sessionsAfter[0]).toBe(result.sessionID);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test("handles multiple sessions in list - skips locked sessions and returns next available", async () => {
|
|
192
|
+
const accountID = account.$jazz.id;
|
|
193
|
+
const session1 = "session-1" as SessionID;
|
|
194
|
+
const session2 = "session-2" as SessionID;
|
|
195
|
+
const session3 = "session-3" as SessionID;
|
|
196
|
+
|
|
197
|
+
// Pre-populate localStorage with multiple sessions
|
|
198
|
+
SessionIDStorage.storeSessionID(accountID, session1, 0);
|
|
199
|
+
SessionIDStorage.storeSessionID(accountID, session2, 1);
|
|
200
|
+
SessionIDStorage.storeSessionID(accountID, session3, 2);
|
|
201
|
+
|
|
202
|
+
// Verify sessions are stored
|
|
203
|
+
const sessionsBefore = SessionIDStorage.getSessionsList(accountID);
|
|
204
|
+
expect(sessionsBefore).toHaveLength(3);
|
|
205
|
+
|
|
206
|
+
// Lock the first session (index 0) by manually adding it to mockLocks
|
|
207
|
+
const lockName = `load_session_${session1}`;
|
|
208
|
+
mockLocks.set(lockName, {
|
|
209
|
+
mode: "exclusive",
|
|
210
|
+
release: () => {
|
|
211
|
+
mockLocks.delete(lockName);
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Acquire session - should skip locked session1 and return session2
|
|
216
|
+
const result = await sessionProvider.acquireSession(
|
|
217
|
+
accountID,
|
|
218
|
+
Crypto as CryptoProvider,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// Verify it returned session2 (next available, not session1 which is locked)
|
|
222
|
+
expect(result.sessionID).toBe(session2);
|
|
223
|
+
|
|
224
|
+
// Verify the returned session is from the existing list, not a new one
|
|
225
|
+
const allSessions = SessionIDStorage.getSessionsList(accountID);
|
|
226
|
+
expect(allSessions).toContain(result.sessionID);
|
|
227
|
+
expect([session1, session2, session3]).toContain(result.sessionID);
|
|
228
|
+
|
|
229
|
+
// Clean up the held lock
|
|
230
|
+
mockLocks.delete(lockName);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("creates new session when all existing sessions are locked", async () => {
|
|
234
|
+
const accountID = account.$jazz.id;
|
|
235
|
+
const session1 = "session-1" as SessionID;
|
|
236
|
+
const session2 = "session-2" as SessionID;
|
|
237
|
+
|
|
238
|
+
// Pre-populate localStorage with sessions
|
|
239
|
+
SessionIDStorage.storeSessionID(accountID, session1, 0);
|
|
240
|
+
SessionIDStorage.storeSessionID(accountID, session2, 1);
|
|
241
|
+
|
|
242
|
+
// Verify sessions are stored
|
|
243
|
+
const sessionsBefore = SessionIDStorage.getSessionsList(accountID);
|
|
244
|
+
expect(sessionsBefore).toHaveLength(2);
|
|
245
|
+
|
|
246
|
+
// Lock all existing sessions by manually adding them to mockLocks
|
|
247
|
+
const lock1Name = `load_session_${session1}`;
|
|
248
|
+
const lock2Name = `load_session_${session2}`;
|
|
249
|
+
mockLocks.set(lock1Name, {
|
|
250
|
+
mode: "exclusive",
|
|
251
|
+
release: () => {
|
|
252
|
+
mockLocks.delete(lock1Name);
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
mockLocks.set(lock2Name, {
|
|
256
|
+
mode: "exclusive",
|
|
257
|
+
release: () => {
|
|
258
|
+
mockLocks.delete(lock2Name);
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
// Acquire session - should create a new one since all existing are locked
|
|
263
|
+
const result = await sessionProvider.acquireSession(
|
|
264
|
+
accountID,
|
|
265
|
+
Crypto as CryptoProvider,
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Verify a new session is created (not one of the existing locked ones)
|
|
269
|
+
expect(result.sessionID).not.toBe(session1);
|
|
270
|
+
expect(result.sessionID).not.toBe(session2);
|
|
271
|
+
|
|
272
|
+
// Verify the new session is added to localStorage
|
|
273
|
+
const sessionsAfter = SessionIDStorage.getSessionsList(accountID);
|
|
274
|
+
expect(sessionsAfter).toHaveLength(3);
|
|
275
|
+
expect(sessionsAfter).toContain(result.sessionID);
|
|
276
|
+
|
|
277
|
+
// Clean up held locks
|
|
278
|
+
mockLocks.delete(lock1Name);
|
|
279
|
+
mockLocks.delete(lock2Name);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
test("releases lock when sessionDone is called", async () => {
|
|
283
|
+
const accountID = account.$jazz.id;
|
|
284
|
+
|
|
285
|
+
// Acquire a session
|
|
286
|
+
const result = await sessionProvider.acquireSession(
|
|
287
|
+
accountID,
|
|
288
|
+
Crypto as CryptoProvider,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const sessionID = result.sessionID;
|
|
292
|
+
|
|
293
|
+
// Call sessionDone to release the lock
|
|
294
|
+
result.sessionDone();
|
|
295
|
+
|
|
296
|
+
// Wait for async lock release
|
|
297
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
298
|
+
|
|
299
|
+
// Acquire a session
|
|
300
|
+
const result2 = await sessionProvider.acquireSession(
|
|
301
|
+
accountID,
|
|
302
|
+
Crypto as CryptoProvider,
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
expect(result2.sessionID).toBe(sessionID);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe("persistSession", () => {
|
|
310
|
+
test("stores session ID correctly", async () => {
|
|
311
|
+
const accountID = account.$jazz.id;
|
|
312
|
+
const sessionID = "test-session-id" as SessionID;
|
|
313
|
+
|
|
314
|
+
// Verify no sessions exist before
|
|
315
|
+
const sessionsBefore = SessionIDStorage.getSessionsList(accountID);
|
|
316
|
+
expect(sessionsBefore).toEqual([]);
|
|
317
|
+
|
|
318
|
+
// Persist session
|
|
319
|
+
const result = await sessionProvider.persistSession(accountID, sessionID);
|
|
320
|
+
|
|
321
|
+
// Verify the session ID is stored in localStorage at index 0
|
|
322
|
+
const storedSessions = SessionIDStorage.getSessionsList(accountID);
|
|
323
|
+
expect(storedSessions).toHaveLength(1);
|
|
324
|
+
expect(storedSessions[0]).toBe(sessionID);
|
|
325
|
+
|
|
326
|
+
// Verify sessionDone callback is provided
|
|
327
|
+
expect(typeof result.sessionDone).toBe("function");
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
test("adds to sessions list properly", async () => {
|
|
331
|
+
const accountID = account.$jazz.id;
|
|
332
|
+
const initialSessionID = "initial-session-id" as SessionID;
|
|
333
|
+
const newSessionID = "new-session-id" as SessionID;
|
|
334
|
+
|
|
335
|
+
// Pre-populate localStorage with one session (index 0)
|
|
336
|
+
SessionIDStorage.storeSessionID(accountID, initialSessionID, 0);
|
|
337
|
+
|
|
338
|
+
// Verify initial session is stored
|
|
339
|
+
const sessionsBefore = SessionIDStorage.getSessionsList(accountID);
|
|
340
|
+
expect(sessionsBefore).toHaveLength(1);
|
|
341
|
+
expect(sessionsBefore[0]).toBe(initialSessionID);
|
|
342
|
+
|
|
343
|
+
// Persist a new session ID
|
|
344
|
+
await sessionProvider.persistSession(accountID, newSessionID);
|
|
345
|
+
|
|
346
|
+
// Verify the new session is stored at index 1
|
|
347
|
+
const sessionsAfter = SessionIDStorage.getSessionsList(accountID);
|
|
348
|
+
expect(sessionsAfter).toHaveLength(2);
|
|
349
|
+
expect(sessionsAfter[0]).toBe(initialSessionID);
|
|
350
|
+
expect(sessionsAfter[1]).toBe(newSessionID);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test("locks session when persisting", async () => {
|
|
354
|
+
const accountID = account.$jazz.id;
|
|
355
|
+
const sessionID = "persisted-session-id" as SessionID;
|
|
356
|
+
|
|
357
|
+
// Persist session - this should acquire a lock on the session
|
|
358
|
+
const { sessionDone } = await sessionProvider.persistSession(
|
|
359
|
+
accountID,
|
|
360
|
+
sessionID,
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
// Verify the session is locked by checking mockLocks directly
|
|
364
|
+
// The lock should be held until sessionDone is called
|
|
365
|
+
const lockName = `load_session_${sessionID}`;
|
|
366
|
+
expect(mockLocks.has(lockName)).toBe(true);
|
|
367
|
+
|
|
368
|
+
// Also verify we can't acquire the lock while it's held
|
|
369
|
+
// (This tests the isLockAvailable function)
|
|
370
|
+
expect(isLockAvailable(lockName, "exclusive")).toBe(false);
|
|
371
|
+
|
|
372
|
+
// Clean up by releasing the lock
|
|
373
|
+
sessionDone();
|
|
374
|
+
|
|
375
|
+
// After releasing, the lock should be removed
|
|
376
|
+
// Note: The lock might be removed asynchronously, so we check after a small delay
|
|
377
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
378
|
+
expect(mockLocks.has(lockName)).toBe(false);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("releases lock when sessionDone is called", async () => {
|
|
382
|
+
const accountID = account.$jazz.id;
|
|
383
|
+
const sessionID = "session-to-release" as SessionID;
|
|
384
|
+
|
|
385
|
+
// Persist a session
|
|
386
|
+
const { sessionDone } = await sessionProvider.persistSession(
|
|
387
|
+
accountID,
|
|
388
|
+
sessionID,
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
// Call sessionDone to release the lock
|
|
392
|
+
sessionDone();
|
|
393
|
+
|
|
394
|
+
// Wait for async lock release
|
|
395
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
396
|
+
|
|
397
|
+
// Acquire a session
|
|
398
|
+
const result = await sessionProvider.acquireSession(
|
|
399
|
+
accountID,
|
|
400
|
+
Crypto as CryptoProvider,
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
expect(result.sessionID).toBe(sessionID);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
});
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { AgentID, CryptoProvider, RawAccountID, SessionID } from "cojson";
|
|
2
|
+
import { ID, Account, SessionProvider } from "jazz-tools";
|
|
3
|
+
import { SessionIDStorage } from "./SessionIDStorage";
|
|
4
|
+
|
|
5
|
+
export class BrowserSessionProvider implements SessionProvider {
|
|
6
|
+
async acquireSession(
|
|
7
|
+
accountID: ID<Account> | AgentID,
|
|
8
|
+
crypto: CryptoProvider,
|
|
9
|
+
): Promise<{ sessionID: SessionID; sessionDone: () => void }> {
|
|
10
|
+
const { sessionPromise, resolveSession } = createSessionLockPromise();
|
|
11
|
+
|
|
12
|
+
// Get the list of sessions for the account, to try to acquire an existing session
|
|
13
|
+
const sessionsList = SessionIDStorage.getSessionsList(accountID);
|
|
14
|
+
|
|
15
|
+
for (const [index, sessionID] of sessionsList.entries()) {
|
|
16
|
+
const sessionAcquired = await tryToAcquireSession(
|
|
17
|
+
sessionID,
|
|
18
|
+
sessionPromise,
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
if (sessionAcquired) {
|
|
22
|
+
console.log("Using existing session", sessionID, "at index", index); // This log is used in the e2e tests to verify the correctness of the feature
|
|
23
|
+
return {
|
|
24
|
+
sessionID,
|
|
25
|
+
sessionDone: resolveSession,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const newSessionID = crypto.newRandomSessionID(
|
|
31
|
+
accountID as RawAccountID | AgentID,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
// Acquire exclusively the session to store the new session ID for reuse in future sessions
|
|
35
|
+
await lockAndStoreSession(accountID, newSessionID, sessionPromise);
|
|
36
|
+
|
|
37
|
+
console.log("Created new session", newSessionID); // This log is used in the e2e tests to verify the correctness of the feature
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
sessionID: newSessionID,
|
|
41
|
+
sessionDone: resolveSession,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async persistSession(
|
|
46
|
+
accountID: ID<Account> | AgentID,
|
|
47
|
+
sessionID: SessionID,
|
|
48
|
+
): Promise<{ sessionDone: () => void }> {
|
|
49
|
+
const { sessionPromise, resolveSession } = createSessionLockPromise();
|
|
50
|
+
|
|
51
|
+
// Store the session id for future use and lock it until the session is done
|
|
52
|
+
await lockAndStoreSession(accountID, sessionID, sessionPromise);
|
|
53
|
+
|
|
54
|
+
console.log("Stored new session", sessionID);
|
|
55
|
+
|
|
56
|
+
return { sessionDone: resolveSession };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function lockAndStoreSession(
|
|
61
|
+
accountID: ID<Account> | AgentID,
|
|
62
|
+
sessionID: SessionID,
|
|
63
|
+
sessionPromise: Promise<void>,
|
|
64
|
+
) {
|
|
65
|
+
const sessionAcquired = await tryToAcquireSession(sessionID, sessionPromise);
|
|
66
|
+
|
|
67
|
+
if (!sessionAcquired) {
|
|
68
|
+
// This should never happen because the session has been randomly generated
|
|
69
|
+
throw new Error("Couldn't get lock on new session");
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// We don't need to wait for this to finish, we only need to acquire the lock on the new session
|
|
73
|
+
storeSessionID(accountID, sessionID);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function tryToAcquireSession(
|
|
77
|
+
sessionID: SessionID,
|
|
78
|
+
sessionDonePromise: Promise<void>,
|
|
79
|
+
) {
|
|
80
|
+
return new Promise<boolean>((resolve) => {
|
|
81
|
+
// Acquire exclusively the session if available
|
|
82
|
+
navigator.locks.request(
|
|
83
|
+
`load_session_${sessionID}`,
|
|
84
|
+
{ mode: "exclusive", ifAvailable: true },
|
|
85
|
+
async (lock) => {
|
|
86
|
+
if (!lock) {
|
|
87
|
+
resolve(false); // Session already in use
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
resolve(true); // Session is available
|
|
92
|
+
|
|
93
|
+
// Return the promise to lock the session until sessionDone is called
|
|
94
|
+
return sessionDonePromise;
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function storeSessionID(
|
|
101
|
+
accountID: ID<Account> | AgentID,
|
|
102
|
+
sessionID: SessionID,
|
|
103
|
+
) {
|
|
104
|
+
return navigator.locks.request(
|
|
105
|
+
`store_session_${accountID}`,
|
|
106
|
+
{ mode: "exclusive" },
|
|
107
|
+
async (lock) => {
|
|
108
|
+
if (!lock) {
|
|
109
|
+
console.error("Couldn't get lock to store session ID", accountID);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const sessionsList = SessionIDStorage.getSessionsList(accountID);
|
|
113
|
+
SessionIDStorage.storeSessionID(
|
|
114
|
+
accountID,
|
|
115
|
+
sessionID,
|
|
116
|
+
sessionsList.length,
|
|
117
|
+
);
|
|
118
|
+
},
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function createSessionLockPromise() {
|
|
123
|
+
let resolveSession: () => void;
|
|
124
|
+
const sessionPromise = new Promise<void>((resolve) => {
|
|
125
|
+
resolveSession = resolve;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
sessionPromise,
|
|
130
|
+
resolveSession: resolveSession!,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { SessionID } from "cojson";
|
|
2
|
+
|
|
3
|
+
function getSessionKey(accountID: string, index: number) {
|
|
4
|
+
return accountID + "_" + index;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export class SessionIDStorage {
|
|
8
|
+
static getSessionsList(accountID: string) {
|
|
9
|
+
let sessionsList: SessionID[] = [];
|
|
10
|
+
let i = 0;
|
|
11
|
+
let lastSessionID: SessionID | null;
|
|
12
|
+
|
|
13
|
+
do {
|
|
14
|
+
lastSessionID = localStorage.getItem(
|
|
15
|
+
getSessionKey(accountID, i),
|
|
16
|
+
) as SessionID | null;
|
|
17
|
+
if (lastSessionID) {
|
|
18
|
+
sessionsList.push(lastSessionID);
|
|
19
|
+
}
|
|
20
|
+
i++;
|
|
21
|
+
} while (lastSessionID);
|
|
22
|
+
|
|
23
|
+
return sessionsList;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static storeSessionID(
|
|
27
|
+
accountID: string,
|
|
28
|
+
sessionID: SessionID,
|
|
29
|
+
index: number,
|
|
30
|
+
) {
|
|
31
|
+
localStorage.setItem(getSessionKey(accountID, index), sessionID);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { MockSessionProvider } from "jazz-tools";
|
|
2
|
+
import { BrowserSessionProvider } from "./BrowserSessionProvider";
|
|
3
|
+
|
|
4
|
+
export function getBrowserLockSessionProvider() {
|
|
5
|
+
if (typeof navigator === "undefined" || !navigator.locks?.request) {
|
|
6
|
+
// Fallback to random session ID for each tab session
|
|
7
|
+
return new MockSessionProvider();
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return new BrowserSessionProvider();
|
|
11
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { assert, describe, expect, it } from "vitest";
|
|
2
|
+
import { setActiveAccount, setupJazzTestSync } from "jazz-tools/testing";
|
|
3
|
+
import { co, z } from "jazz-tools";
|
|
4
|
+
import * as TransactionsChanges from "../../utils/transactions-changes";
|
|
5
|
+
|
|
6
|
+
describe("transactions changes", async () => {
|
|
7
|
+
const account = await setupJazzTestSync();
|
|
8
|
+
setActiveAccount(account);
|
|
9
|
+
|
|
10
|
+
describe("ambiguous values in Group's transactions", () => {
|
|
11
|
+
it("isGroupExtension should return false for a CoMap", () => {
|
|
12
|
+
const value = co.map({ test: z.string() }).create({ test: "extend" })
|
|
13
|
+
.$jazz.raw;
|
|
14
|
+
|
|
15
|
+
const transactions = value.core.verifiedTransactions;
|
|
16
|
+
expect(
|
|
17
|
+
TransactionsChanges.isGroupExtension(
|
|
18
|
+
value,
|
|
19
|
+
transactions[0]?.changes?.[0],
|
|
20
|
+
),
|
|
21
|
+
).toBe(false);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("isGroupExtendRevocation should return false for a CoMap", () => {
|
|
25
|
+
const value = co.map({ test: z.string() }).create({ test: "revoked" })
|
|
26
|
+
.$jazz.raw;
|
|
27
|
+
|
|
28
|
+
const transactions = value.core.verifiedTransactions;
|
|
29
|
+
expect(
|
|
30
|
+
TransactionsChanges.isGroupExtendRevocation(
|
|
31
|
+
value,
|
|
32
|
+
transactions[0]?.changes?.[0],
|
|
33
|
+
),
|
|
34
|
+
).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("isGroupPromotion should return false for a CoMap", () => {
|
|
38
|
+
const value = co
|
|
39
|
+
.map({ parent_co_test: z.string() })
|
|
40
|
+
.create({ parent_co_test: "foo" }).$jazz.raw;
|
|
41
|
+
|
|
42
|
+
const transactions = value.core.verifiedTransactions;
|
|
43
|
+
expect(
|
|
44
|
+
TransactionsChanges.isGroupPromotion(
|
|
45
|
+
value,
|
|
46
|
+
transactions[0]?.changes?.[0],
|
|
47
|
+
),
|
|
48
|
+
).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("isUserPromotion should return false for a CoMap", () => {
|
|
52
|
+
const value = co.map({ everyone: z.string() }).create({ everyone: "foo" })
|
|
53
|
+
.$jazz.raw;
|
|
54
|
+
|
|
55
|
+
const transactions = value.core.verifiedTransactions;
|
|
56
|
+
expect(
|
|
57
|
+
TransactionsChanges.isUserPromotion(
|
|
58
|
+
value,
|
|
59
|
+
transactions[0]?.changes?.[0],
|
|
60
|
+
),
|
|
61
|
+
).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("isUserPromotion should return false for a CoMap", () => {
|
|
65
|
+
const value = co.map({ everyone: z.string() }).create({ everyone: "foo" })
|
|
66
|
+
.$jazz.raw;
|
|
67
|
+
|
|
68
|
+
const transactions = value.core.verifiedTransactions;
|
|
69
|
+
expect(
|
|
70
|
+
TransactionsChanges.isUserPromotion(
|
|
71
|
+
value,
|
|
72
|
+
transactions[0]?.changes?.[0],
|
|
73
|
+
),
|
|
74
|
+
).toBe(false);
|
|
75
|
+
|
|
76
|
+
const value2 = co.map({ co_z123: z.string() }).create({ co_z123: "foo" })
|
|
77
|
+
.$jazz.raw;
|
|
78
|
+
|
|
79
|
+
const transactions2 = value2.core.verifiedTransactions;
|
|
80
|
+
expect(
|
|
81
|
+
TransactionsChanges.isUserPromotion(
|
|
82
|
+
value2,
|
|
83
|
+
transactions2[0]?.changes?.[0],
|
|
84
|
+
),
|
|
85
|
+
).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("isKeyRevelation should return false for a CoMap", () => {
|
|
89
|
+
const value = co
|
|
90
|
+
.map({ "123_for_test": z.string() })
|
|
91
|
+
.create({ "123_for_test": "foo" }).$jazz.raw;
|
|
92
|
+
|
|
93
|
+
const transactions = value.core.verifiedTransactions;
|
|
94
|
+
expect(
|
|
95
|
+
TransactionsChanges.isKeyRevelation(
|
|
96
|
+
value,
|
|
97
|
+
transactions[0]?.changes?.[0],
|
|
98
|
+
),
|
|
99
|
+
).toBe(false);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -12,9 +12,9 @@ export function AddIcon(props: React.SVGProps<SVGSVGElement>) {
|
|
|
12
12
|
>
|
|
13
13
|
<path
|
|
14
14
|
d="M4 12H20M12 4V20"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
strokeWidth="2"
|
|
16
|
+
strokeLinecap="round"
|
|
17
|
+
strokeLinejoin="round"
|
|
18
18
|
/>
|
|
19
19
|
</svg>
|
|
20
20
|
);
|