jazz-tools 0.20.18 → 0.20.19
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 +93 -93
- package/CHANGELOG.md +15 -0
- package/dist/browser/index.js +36 -9
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/provideBrowserLockSession/BrowserSessionDurabilityMarker.d.ts +8 -0
- package/dist/browser/provideBrowserLockSession/BrowserSessionDurabilityMarker.d.ts.map +1 -0
- package/dist/browser/provideBrowserLockSession/BrowserSessionProvider.d.ts +1 -0
- package/dist/browser/provideBrowserLockSession/BrowserSessionProvider.d.ts.map +1 -1
- package/dist/browser/provideBrowserLockSession/index.d.ts +1 -0
- package/dist/browser/provideBrowserLockSession/index.d.ts.map +1 -1
- package/dist/{chunk-MIPBSAS7.js → chunk-VPK4IGIA.js} +57 -1
- package/dist/chunk-VPK4IGIA.js.map +1 -0
- package/dist/expo/index.js +22 -7
- package/dist/expo/index.js.map +1 -1
- package/dist/expo/storage/expo-sqlite-adapter.d.ts +9 -0
- package/dist/expo/storage/expo-sqlite-adapter.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/react-native/index.js +42 -4
- package/dist/react-native/index.js.map +1 -1
- package/dist/react-native-core/ReactNativeSessionDurabilityMarker.d.ts +8 -0
- package/dist/react-native-core/ReactNativeSessionDurabilityMarker.d.ts.map +1 -0
- package/dist/react-native-core/ReactNativeSessionProvider.d.ts +1 -0
- package/dist/react-native-core/ReactNativeSessionProvider.d.ts.map +1 -1
- package/dist/react-native-core/index.d.ts +1 -0
- package/dist/react-native-core/index.d.ts.map +1 -1
- package/dist/react-native-core/index.js +42 -4
- package/dist/react-native-core/index.js.map +1 -1
- package/dist/testing.js +1 -1
- package/dist/tools/exports.d.ts +1 -1
- package/dist/tools/exports.d.ts.map +1 -1
- package/dist/tools/implementation/createContext.d.ts +8 -0
- package/dist/tools/implementation/createContext.d.ts.map +1 -1
- package/dist/tools/implementation/sessionDurabilityMarker.d.ts +38 -0
- package/dist/tools/implementation/sessionDurabilityMarker.d.ts.map +1 -0
- package/dist/tools/internal.d.ts +1 -0
- package/dist/tools/internal.d.ts.map +1 -1
- package/dist/tools/tests/sessionDurabilityMarker.test.d.ts +2 -0
- package/dist/tools/tests/sessionDurabilityMarker.test.d.ts.map +1 -0
- package/package.json +4 -4
- package/src/browser/provideBrowserLockSession/BrowserSessionDurabilityMarker.ts +20 -0
- package/src/browser/provideBrowserLockSession/BrowserSessionProvider.test.ts +97 -1
- package/src/browser/provideBrowserLockSession/BrowserSessionProvider.ts +37 -8
- package/src/browser/provideBrowserLockSession/index.ts +1 -0
- package/src/expo/storage/expo-sqlite-adapter.ts +24 -7
- package/src/expo/tests/expo-sqlite-adapter.test.ts +94 -0
- package/src/react-native-core/ReactNativeSessionDurabilityMarker.ts +40 -0
- package/src/react-native-core/ReactNativeSessionProvider.ts +24 -1
- package/src/react-native-core/index.ts +1 -0
- package/src/react-native-core/tests/ReactNativeSessionProvider.test.ts +42 -0
- package/src/tools/exports.ts +3 -0
- package/src/tools/implementation/createContext.ts +30 -0
- package/src/tools/implementation/sessionDurabilityMarker.ts +93 -0
- package/src/tools/internal.ts +1 -0
- package/src/tools/tests/sessionDurabilityMarker.test.ts +183 -0
- package/dist/chunk-MIPBSAS7.js.map +0 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { LocalStoreDurabilityListener, SessionID } from "cojson";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Persists a per-session "dirty" flag that survives crashes.
|
|
5
|
+
*
|
|
6
|
+
* The flag is set while the session has transactions that were sent to a sync
|
|
7
|
+
* server but are not yet durably stored locally. If the process dies inside
|
|
8
|
+
* that window, local storage is behind what the server received for the
|
|
9
|
+
* session, and reusing it would fork the session's hash chain. Session
|
|
10
|
+
* providers must skip sessions whose flag is still set and mint a fresh
|
|
11
|
+
* session instead.
|
|
12
|
+
*/
|
|
13
|
+
export interface SessionDurabilityMarker {
|
|
14
|
+
/**
|
|
15
|
+
* Must be initiated synchronously: the write has to win the race against
|
|
16
|
+
* the network send that immediately follows it.
|
|
17
|
+
*/
|
|
18
|
+
set(sessionID: SessionID): void;
|
|
19
|
+
clear(sessionID: SessionID): void;
|
|
20
|
+
isSet(sessionID: SessionID): boolean | Promise<boolean>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Storage key under which a session's dirty flag is persisted. Shared by all
|
|
25
|
+
* platform marker implementations so the key format cannot silently diverge.
|
|
26
|
+
*/
|
|
27
|
+
export function sessionDurabilityMarkerKey(sessionID: SessionID) {
|
|
28
|
+
return `jazz_session_dirty_${sessionID}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const SESSION_DURABILITY_CLEAR_DEBOUNCE_MS = 200;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Adapts a SessionDurabilityMarker to LocalNode's onLocalStoreDurabilityChange
|
|
35
|
+
* hook. Setting is immediate (correctness-critical); clearing is debounced so
|
|
36
|
+
* the marker doesn't churn on every batch while the user is actively editing.
|
|
37
|
+
* A crash inside the debounce at worst abandons one session unnecessarily.
|
|
38
|
+
*
|
|
39
|
+
* Returns undefined when no marker is provided, matching the optional
|
|
40
|
+
* listener slot on LocalNode creation options.
|
|
41
|
+
*/
|
|
42
|
+
export function makeDurabilityMarkerListener(
|
|
43
|
+
marker: SessionDurabilityMarker,
|
|
44
|
+
clearDebounceMs?: number,
|
|
45
|
+
): LocalStoreDurabilityListener;
|
|
46
|
+
export function makeDurabilityMarkerListener(
|
|
47
|
+
marker: SessionDurabilityMarker | undefined,
|
|
48
|
+
clearDebounceMs?: number,
|
|
49
|
+
): LocalStoreDurabilityListener | undefined;
|
|
50
|
+
export function makeDurabilityMarkerListener(
|
|
51
|
+
marker: SessionDurabilityMarker | undefined,
|
|
52
|
+
clearDebounceMs: number = SESSION_DURABILITY_CLEAR_DEBOUNCE_MS,
|
|
53
|
+
): LocalStoreDurabilityListener | undefined {
|
|
54
|
+
if (!marker) {
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let clearTimer: ReturnType<typeof setTimeout> | undefined;
|
|
59
|
+
let markerIsSet = false;
|
|
60
|
+
|
|
61
|
+
return (hasPending, sessionID) => {
|
|
62
|
+
if (clearTimer !== undefined) {
|
|
63
|
+
clearTimeout(clearTimer);
|
|
64
|
+
clearTimer = undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (hasPending) {
|
|
68
|
+
// A window reopening within the clear debounce finds the marker still
|
|
69
|
+
// set: skip the write so steady-state editing costs no marker I/O
|
|
70
|
+
if (markerIsSet) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
marker.set(sessionID);
|
|
76
|
+
markerIsSet = true;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
console.warn("Failed to set session durability marker", err);
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
clearTimer = setTimeout(() => {
|
|
82
|
+
clearTimer = undefined;
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
marker.clear(sessionID);
|
|
86
|
+
markerIsSet = false;
|
|
87
|
+
} catch (err) {
|
|
88
|
+
console.warn("Failed to clear session durability marker", err);
|
|
89
|
+
}
|
|
90
|
+
}, clearDebounceMs);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
package/src/tools/internal.ts
CHANGED
|
@@ -33,6 +33,7 @@ export * from "./lib/cache.js";
|
|
|
33
33
|
export * from "./lib/utils.js";
|
|
34
34
|
export * from "./lib/utilityTypes.js";
|
|
35
35
|
export * from "./implementation/createContext.js";
|
|
36
|
+
export * from "./implementation/sessionDurabilityMarker.js";
|
|
36
37
|
|
|
37
38
|
export * from "./types.js";
|
|
38
39
|
export * from "./implementation/zodSchema/zodSchema.js";
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import type { SessionID } from "cojson";
|
|
2
|
+
import { WasmCrypto } from "cojson/crypto/WasmCrypto";
|
|
3
|
+
import { LocalNode } from "cojson";
|
|
4
|
+
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
5
|
+
import { makeDurabilityMarkerListener } from "../implementation/sessionDurabilityMarker.js";
|
|
6
|
+
import {
|
|
7
|
+
Credentials,
|
|
8
|
+
InMemoryKVStore,
|
|
9
|
+
KvStoreContext,
|
|
10
|
+
MockSessionProvider,
|
|
11
|
+
createJazzContextFromExistingCredentials,
|
|
12
|
+
} from "../exports.js";
|
|
13
|
+
import {
|
|
14
|
+
createJazzTestAccount,
|
|
15
|
+
getPeerConnectedToTestSyncServer,
|
|
16
|
+
setupJazzTestSync,
|
|
17
|
+
} from "../testing.js";
|
|
18
|
+
|
|
19
|
+
const sessionID = "co_ztest_session_ztest" as SessionID;
|
|
20
|
+
|
|
21
|
+
function mockMarker() {
|
|
22
|
+
return { set: vi.fn(), clear: vi.fn(), isSet: vi.fn(() => false) };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
vi.useRealTimers();
|
|
27
|
+
vi.restoreAllMocks();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("makeDurabilityMarkerListener", () => {
|
|
31
|
+
test("sets the marker synchronously when the window opens", () => {
|
|
32
|
+
const marker = mockMarker();
|
|
33
|
+
const listener = makeDurabilityMarkerListener(marker);
|
|
34
|
+
|
|
35
|
+
listener(true, sessionID);
|
|
36
|
+
|
|
37
|
+
expect(marker.set).toHaveBeenCalledWith(sessionID);
|
|
38
|
+
expect(marker.clear).not.toHaveBeenCalled();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("clears the marker only after the debounce delay", () => {
|
|
42
|
+
vi.useFakeTimers();
|
|
43
|
+
const marker = mockMarker();
|
|
44
|
+
const listener = makeDurabilityMarkerListener(marker, 200);
|
|
45
|
+
|
|
46
|
+
listener(true, sessionID);
|
|
47
|
+
listener(false, sessionID);
|
|
48
|
+
expect(marker.clear).not.toHaveBeenCalled();
|
|
49
|
+
|
|
50
|
+
vi.advanceTimersByTime(200);
|
|
51
|
+
expect(marker.clear).toHaveBeenCalledWith(sessionID);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("a new pending window cancels a scheduled clear", () => {
|
|
55
|
+
vi.useFakeTimers();
|
|
56
|
+
const marker = mockMarker();
|
|
57
|
+
const listener = makeDurabilityMarkerListener(marker, 200);
|
|
58
|
+
|
|
59
|
+
listener(true, sessionID);
|
|
60
|
+
listener(false, sessionID);
|
|
61
|
+
listener(true, sessionID); // window re-opens within the debounce
|
|
62
|
+
vi.advanceTimersByTime(500);
|
|
63
|
+
|
|
64
|
+
expect(marker.clear).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("consecutive clears don't leave an orphaned timer that clears a fresh set", () => {
|
|
68
|
+
vi.useFakeTimers();
|
|
69
|
+
const marker = mockMarker();
|
|
70
|
+
const listener = makeDurabilityMarkerListener(marker, 200);
|
|
71
|
+
|
|
72
|
+
listener(true, sessionID);
|
|
73
|
+
listener(false, sessionID);
|
|
74
|
+
listener(false, sessionID); // must supersede (not orphan) the first timer
|
|
75
|
+
listener(true, sessionID); // window re-opens: no clear may fire anymore
|
|
76
|
+
vi.advanceTimersByTime(500);
|
|
77
|
+
|
|
78
|
+
expect(marker.clear).not.toHaveBeenCalled();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("a reopen within the debounce doesn't rewrite the still-set marker", () => {
|
|
82
|
+
vi.useFakeTimers();
|
|
83
|
+
const marker = mockMarker();
|
|
84
|
+
const listener = makeDurabilityMarkerListener(marker, 200);
|
|
85
|
+
|
|
86
|
+
listener(true, sessionID);
|
|
87
|
+
listener(false, sessionID);
|
|
88
|
+
listener(true, sessionID); // marker was never cleared: no write needed
|
|
89
|
+
|
|
90
|
+
expect(marker.set).toHaveBeenCalledTimes(1);
|
|
91
|
+
|
|
92
|
+
// ...but after an actual clear, the next window writes again
|
|
93
|
+
listener(false, sessionID);
|
|
94
|
+
vi.advanceTimersByTime(200);
|
|
95
|
+
listener(true, sessionID);
|
|
96
|
+
|
|
97
|
+
expect(marker.clear).toHaveBeenCalledTimes(1);
|
|
98
|
+
expect(marker.set).toHaveBeenCalledTimes(2);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("returns undefined when no marker is provided", () => {
|
|
102
|
+
expect(makeDurabilityMarkerListener(undefined)).toBeUndefined();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("marker errors are swallowed with a warning", () => {
|
|
106
|
+
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
107
|
+
const marker = mockMarker();
|
|
108
|
+
marker.set.mockImplementation(() => {
|
|
109
|
+
throw new Error("quota exceeded");
|
|
110
|
+
});
|
|
111
|
+
const listener = makeDurabilityMarkerListener(marker);
|
|
112
|
+
|
|
113
|
+
expect(() => listener(true, sessionID)).not.toThrow();
|
|
114
|
+
expect(warn).toHaveBeenCalled();
|
|
115
|
+
warn.mockRestore();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const Crypto = await WasmCrypto.create();
|
|
120
|
+
KvStoreContext.getInstance().initialize(new InMemoryKVStore());
|
|
121
|
+
|
|
122
|
+
describe("createContext wiring", () => {
|
|
123
|
+
test("passes a durability listener to LocalNode when the provider has a marker", async () => {
|
|
124
|
+
await setupJazzTestSync();
|
|
125
|
+
const account = await createJazzTestAccount({
|
|
126
|
+
isCurrentActiveAccount: true,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const credentials: Credentials = {
|
|
130
|
+
accountID: account.$jazz.id,
|
|
131
|
+
secret: account.$jazz.localNode.getCurrentAgent().agentSecret,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const spy = vi.spyOn(LocalNode, "withLoadedAccount");
|
|
135
|
+
|
|
136
|
+
const provider = new MockSessionProvider();
|
|
137
|
+
provider.durabilityMarker = mockMarker();
|
|
138
|
+
|
|
139
|
+
const context = await createJazzContextFromExistingCredentials({
|
|
140
|
+
credentials,
|
|
141
|
+
peers: [getPeerConnectedToTestSyncServer()],
|
|
142
|
+
crypto: Crypto,
|
|
143
|
+
sessionProvider: provider,
|
|
144
|
+
asActiveAccount: false,
|
|
145
|
+
});
|
|
146
|
+
context.done();
|
|
147
|
+
|
|
148
|
+
expect(spy).toHaveBeenCalledWith(
|
|
149
|
+
expect.objectContaining({
|
|
150
|
+
onLocalStoreDurabilityChange: expect.any(Function),
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("passes no listener when the provider has no marker", async () => {
|
|
156
|
+
await setupJazzTestSync();
|
|
157
|
+
const account = await createJazzTestAccount({
|
|
158
|
+
isCurrentActiveAccount: true,
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
const credentials: Credentials = {
|
|
162
|
+
accountID: account.$jazz.id,
|
|
163
|
+
secret: account.$jazz.localNode.getCurrentAgent().agentSecret,
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const spy = vi.spyOn(LocalNode, "withLoadedAccount");
|
|
167
|
+
|
|
168
|
+
const context = await createJazzContextFromExistingCredentials({
|
|
169
|
+
credentials,
|
|
170
|
+
peers: [getPeerConnectedToTestSyncServer()],
|
|
171
|
+
crypto: Crypto,
|
|
172
|
+
sessionProvider: new MockSessionProvider(),
|
|
173
|
+
asActiveAccount: false,
|
|
174
|
+
});
|
|
175
|
+
context.done();
|
|
176
|
+
|
|
177
|
+
expect(spy).toHaveBeenCalledWith(
|
|
178
|
+
expect.objectContaining({
|
|
179
|
+
onLocalStoreDurabilityChange: undefined,
|
|
180
|
+
}),
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
});
|