jazz-tools 0.18.16 → 0.18.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__/jazz.class.svelte.d.ts +14 -0
- package/.svelte-kit/__package__/jazz.class.svelte.d.ts.map +1 -1
- package/.svelte-kit/__package__/jazz.class.svelte.js +37 -0
- package/.svelte-kit/__package__/media/image.svelte +104 -98
- package/.svelte-kit/__package__/media/image.svelte.d.ts.map +1 -1
- package/.svelte-kit/__package__/testing.d.ts +1 -1
- package/.svelte-kit/__package__/testing.d.ts.map +1 -1
- package/.svelte-kit/__package__/testing.js +1 -1
- package/.svelte-kit/__package__/tests/TestConnectionStatus.svelte +8 -0
- package/.svelte-kit/__package__/tests/TestConnectionStatus.svelte.d.ts +27 -0
- package/.svelte-kit/__package__/tests/TestConnectionStatus.svelte.d.ts.map +1 -0
- package/.svelte-kit/__package__/tests/media/image.svelte.test.js +16 -2
- package/.svelte-kit/__package__/tests/sync-connection-status.svelte.test.d.ts +2 -0
- package/.svelte-kit/__package__/tests/sync-connection-status.svelte.test.d.ts.map +1 -0
- package/.svelte-kit/__package__/tests/sync-connection-status.svelte.test.js +47 -0
- package/.turbo/turbo-build.log +52 -52
- package/CHANGELOG.md +27 -0
- package/dist/browser/BrowserContextManager.d.ts +4 -0
- package/dist/browser/BrowserContextManager.d.ts.map +1 -1
- package/dist/browser/createBrowserContext.d.ts +4 -0
- package/dist/browser/createBrowserContext.d.ts.map +1 -1
- package/dist/browser/index.js +36 -4
- package/dist/browser/index.js.map +1 -1
- package/dist/{chunk-GRN6OAUX.js → chunk-FHRKDKDY.js} +80 -6
- package/dist/chunk-FHRKDKDY.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/react/hooks.d.ts +1 -1
- package/dist/react/hooks.d.ts.map +1 -1
- package/dist/react/index.d.ts +1 -1
- package/dist/react/index.d.ts.map +1 -1
- package/dist/react/index.js +6 -2
- package/dist/react/index.js.map +1 -1
- package/dist/react/media/image.d.ts.map +1 -1
- package/dist/react-core/hooks.d.ts +26 -0
- package/dist/react-core/hooks.d.ts.map +1 -1
- package/dist/react-core/index.js +16 -1
- package/dist/react-core/index.js.map +1 -1
- package/dist/react-core/testing.d.ts +1 -1
- package/dist/react-core/testing.d.ts.map +1 -1
- package/dist/react-core/testing.js +3 -1
- package/dist/react-core/testing.js.map +1 -1
- package/dist/react-core/tests/useSyncConnectionStatus.test.d.ts +2 -0
- package/dist/react-core/tests/useSyncConnectionStatus.test.d.ts.map +1 -0
- package/dist/react-native-core/ReactNativeContextManager.d.ts +4 -0
- package/dist/react-native-core/ReactNativeContextManager.d.ts.map +1 -1
- package/dist/react-native-core/hooks.d.ts +1 -1
- package/dist/react-native-core/hooks.d.ts.map +1 -1
- package/dist/react-native-core/index.js +41 -7
- package/dist/react-native-core/index.js.map +1 -1
- package/dist/react-native-core/media/image.d.ts.map +1 -1
- package/dist/react-native-core/platform.d.ts +4 -0
- package/dist/react-native-core/platform.d.ts.map +1 -1
- package/dist/svelte/jazz.class.svelte.d.ts +14 -0
- package/dist/svelte/jazz.class.svelte.d.ts.map +1 -1
- package/dist/svelte/jazz.class.svelte.js +37 -0
- package/dist/svelte/media/image.svelte +104 -98
- package/dist/svelte/media/image.svelte.d.ts.map +1 -1
- package/dist/svelte/testing.d.ts +1 -1
- package/dist/svelte/testing.d.ts.map +1 -1
- package/dist/svelte/testing.js +1 -1
- package/dist/svelte/tests/TestConnectionStatus.svelte +8 -0
- package/dist/svelte/tests/TestConnectionStatus.svelte.d.ts +27 -0
- package/dist/svelte/tests/TestConnectionStatus.svelte.d.ts.map +1 -0
- package/dist/svelte/tests/media/image.svelte.test.js +16 -2
- package/dist/svelte/tests/sync-connection-status.svelte.test.d.ts +2 -0
- package/dist/svelte/tests/sync-connection-status.svelte.test.d.ts.map +1 -0
- package/dist/svelte/tests/sync-connection-status.svelte.test.js +47 -0
- package/dist/testing.js +34 -4
- package/dist/testing.js.map +1 -1
- package/dist/tools/implementation/ContextManager.d.ts +4 -0
- package/dist/tools/implementation/ContextManager.d.ts.map +1 -1
- package/dist/tools/implementation/refs.d.ts +1 -1
- package/dist/tools/implementation/refs.d.ts.map +1 -1
- package/dist/tools/subscribe/CoValueCoreSubscription.d.ts +4 -0
- package/dist/tools/subscribe/CoValueCoreSubscription.d.ts.map +1 -1
- package/dist/tools/subscribe/SubscriptionScope.d.ts +7 -0
- package/dist/tools/subscribe/SubscriptionScope.d.ts.map +1 -1
- package/dist/tools/subscribe/index.d.ts.map +1 -1
- package/dist/tools/testing.d.ts +8 -0
- package/dist/tools/testing.d.ts.map +1 -1
- package/dist/tools/types.d.ts +4 -0
- package/dist/tools/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/browser/createBrowserContext.ts +34 -4
- package/src/react/hooks.tsx +1 -0
- package/src/react/index.ts +1 -0
- package/src/react/media/image.tsx +2 -0
- package/src/react/tests/media/image.test.tsx +20 -2
- package/src/react-core/hooks.ts +42 -0
- package/src/react-core/testing.tsx +1 -0
- package/src/react-core/tests/useAccountWithSelector.test.ts +98 -2
- package/src/react-core/tests/useSyncConnectionStatus.test.ts +48 -0
- package/src/react-native-core/hooks.tsx +1 -0
- package/src/react-native-core/media/image.tsx +4 -1
- package/src/react-native-core/platform.ts +32 -4
- package/src/svelte/jazz.class.svelte.ts +44 -0
- package/src/svelte/media/image.svelte +104 -98
- package/src/svelte/testing.ts +1 -0
- package/src/svelte/tests/TestConnectionStatus.svelte +8 -0
- package/src/svelte/tests/media/image.svelte.test.ts +18 -2
- package/src/svelte/tests/sync-connection-status.svelte.test.ts +61 -0
- package/src/tools/implementation/ContextManager.ts +8 -0
- package/src/tools/implementation/refs.ts +27 -3
- package/src/tools/subscribe/CoValueCoreSubscription.ts +14 -0
- package/src/tools/subscribe/SubscriptionScope.ts +67 -2
- package/src/tools/subscribe/index.ts +8 -0
- package/src/tools/testing.ts +29 -0
- package/src/tools/tests/ContextManager.test.ts +2 -2
- package/src/tools/tests/coMap.test.ts +42 -0
- package/src/tools/tests/subscribe.test.ts +1 -4
- package/src/tools/types.ts +4 -0
- package/dist/chunk-GRN6OAUX.js.map +0 -1
@@ -205,3 +205,47 @@ export class AccountCoState<
|
|
205
205
|
return this.#isAuthenticated.current;
|
206
206
|
}
|
207
207
|
}
|
208
|
+
|
209
|
+
/**
|
210
|
+
* Class that provides the current connection status to the Jazz sync server.
|
211
|
+
*
|
212
|
+
* @returns `true` when connected to the server, `false` when disconnected
|
213
|
+
*
|
214
|
+
* @remarks
|
215
|
+
* On connection drop, this will return `false` only when Jazz detects the disconnection
|
216
|
+
* after 5 seconds of not receiving a ping from the server.
|
217
|
+
*/
|
218
|
+
export class SyncConnectionStatus {
|
219
|
+
#ctx = getJazzContext<InstanceOfSchema<AccountClass<Account>>>();
|
220
|
+
#subscribe: () => void;
|
221
|
+
#update = () => {};
|
222
|
+
|
223
|
+
constructor() {
|
224
|
+
this.#subscribe = createSubscriber((update) => {
|
225
|
+
this.#update = update;
|
226
|
+
});
|
227
|
+
|
228
|
+
$effect.pre(() => {
|
229
|
+
const ctx = this.#ctx.current;
|
230
|
+
|
231
|
+
return untrack(() => {
|
232
|
+
if (!ctx) {
|
233
|
+
return;
|
234
|
+
}
|
235
|
+
|
236
|
+
const unsubscribe = ctx.addConnectionListener(() => {
|
237
|
+
this.#update();
|
238
|
+
});
|
239
|
+
|
240
|
+
return () => {
|
241
|
+
unsubscribe();
|
242
|
+
};
|
243
|
+
});
|
244
|
+
});
|
245
|
+
}
|
246
|
+
|
247
|
+
get current() {
|
248
|
+
this.#subscribe();
|
249
|
+
return this.#ctx.current?.connected() ?? false;
|
250
|
+
}
|
251
|
+
}
|
@@ -1,118 +1,122 @@
|
|
1
1
|
<script lang="ts">
|
2
|
-
import { ImageDefinition } from "jazz-tools";
|
3
|
-
import { highestResAvailable } from "jazz-tools/media";
|
4
|
-
import { onDestroy } from "svelte";
|
5
|
-
import { CoState } from "../jazz.class.svelte";
|
6
|
-
import type { ImageProps } from "./image.types.js";
|
7
|
-
|
8
|
-
const { imageId, width, height, ...rest }: ImageProps = $props();
|
9
|
-
|
10
|
-
const imageState = new CoState(ImageDefinition, () => imageId);
|
11
|
-
let lastBestImage: [string, string] | null = null;
|
12
|
-
|
13
|
-
/**
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
let waitingLazyLoading = $state(rest.loading === "lazy");
|
20
|
-
const lazyPlaceholder = $derived.by(() =>
|
21
|
-
|
22
|
-
);
|
23
|
-
|
24
|
-
const dimensions = $derived.by<{
|
25
|
-
|
26
|
-
|
27
|
-
}>(() => {
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
// Both width and height are "original"
|
32
|
-
if (width === "original" && height === "original") {
|
33
|
-
return { width: originalWidth, height: originalHeight };
|
34
|
-
}
|
2
|
+
import { ImageDefinition } from "jazz-tools";
|
3
|
+
import { highestResAvailable } from "jazz-tools/media";
|
4
|
+
import { onDestroy } from "svelte";
|
5
|
+
import { CoState } from "../jazz.class.svelte";
|
6
|
+
import type { ImageProps } from "./image.types.js";
|
7
|
+
|
8
|
+
const { imageId, width, height, ...rest }: ImageProps = $props();
|
9
|
+
|
10
|
+
const imageState = new CoState(ImageDefinition, () => imageId);
|
11
|
+
let lastBestImage: [string, string] | null = null;
|
12
|
+
|
13
|
+
/**
|
14
|
+
* For lazy loading, we use the browser's strategy for images with loading="lazy".
|
15
|
+
* We use an empty image, and when the browser triggers the load event, we load the best available image.
|
16
|
+
* On page loading, if the image url is already in browser's cache, the load event is triggered immediately.
|
17
|
+
* This is why we need to use a different blob url for every image.
|
18
|
+
*/
|
19
|
+
let waitingLazyLoading = $state(rest.loading === "lazy");
|
20
|
+
const lazyPlaceholder = $derived.by(() =>
|
21
|
+
waitingLazyLoading ? URL.createObjectURL(emptyPixelBlob) : undefined,
|
22
|
+
);
|
23
|
+
|
24
|
+
const dimensions = $derived.by<{
|
25
|
+
width: number | undefined;
|
26
|
+
height: number | undefined;
|
27
|
+
}>(() => {
|
28
|
+
const originalWidth = imageState.current?.originalSize?.[0];
|
29
|
+
const originalHeight = imageState.current?.originalSize?.[1];
|
35
30
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
return {
|
40
|
-
width: Math.round((height * originalWidth) / originalHeight),
|
41
|
-
height,
|
42
|
-
};
|
31
|
+
// Both width and height are "original"
|
32
|
+
if (width === "original" && height === "original") {
|
33
|
+
return { width: originalWidth, height: originalHeight };
|
43
34
|
}
|
44
|
-
return { width: undefined, height };
|
45
|
-
}
|
46
35
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
36
|
+
// Width is "original", height is a number
|
37
|
+
if (width === "original" && typeof height === "number") {
|
38
|
+
if (originalWidth && originalHeight) {
|
39
|
+
return {
|
40
|
+
width: Math.round((height * originalWidth) / originalHeight),
|
41
|
+
height,
|
42
|
+
};
|
43
|
+
}
|
44
|
+
return { width: undefined, height };
|
54
45
|
}
|
55
|
-
return { width, height: undefined };
|
56
|
-
}
|
57
46
|
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
47
|
+
// Height is "original", width is a number
|
48
|
+
if (height === "original" && typeof width === "number") {
|
49
|
+
if (originalWidth && originalHeight) {
|
50
|
+
return {
|
51
|
+
width,
|
52
|
+
height: Math.round((width * originalHeight) / originalWidth),
|
53
|
+
};
|
54
|
+
}
|
55
|
+
return { width, height: undefined };
|
56
|
+
}
|
64
57
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
58
|
+
// In all other cases, use the property value:
|
59
|
+
return {
|
60
|
+
width: width === "original" ? originalWidth : width,
|
61
|
+
height: height === "original" ? originalHeight : height,
|
62
|
+
};
|
63
|
+
});
|
69
64
|
|
70
|
-
const
|
71
|
-
|
65
|
+
const src = $derived.by(() => {
|
66
|
+
if (waitingLazyLoading) {
|
67
|
+
return lazyPlaceholder;
|
68
|
+
}
|
72
69
|
|
73
|
-
|
74
|
-
image
|
75
|
-
|
76
|
-
dimensions.height || dimensions.width || 9999,
|
77
|
-
);
|
70
|
+
const image = imageState.current;
|
71
|
+
if (image === undefined)
|
72
|
+
return "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==";
|
78
73
|
|
79
|
-
|
80
|
-
if (lastBestImage?.[0] === bestImage.image.$jazz.id) return lastBestImage?.[1];
|
74
|
+
if (!image) return undefined;
|
81
75
|
|
82
|
-
|
76
|
+
const bestImage = highestResAvailable(
|
77
|
+
image,
|
78
|
+
dimensions.width || dimensions.height || 9999,
|
79
|
+
dimensions.height || dimensions.width || 9999,
|
80
|
+
);
|
83
81
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
82
|
+
if (!bestImage) return image.placeholderDataURL;
|
83
|
+
if (lastBestImage?.[0] === bestImage.image.$jazz.id)
|
84
|
+
return lastBestImage?.[1];
|
85
|
+
|
86
|
+
const blob = bestImage.image.toBlob();
|
87
|
+
|
88
|
+
if (blob) {
|
89
|
+
const url = URL.createObjectURL(blob);
|
90
|
+
revokeObjectURL(lastBestImage?.[1]);
|
91
|
+
lastBestImage = [bestImage.image.$jazz.id, url];
|
92
|
+
return url;
|
93
|
+
}
|
90
94
|
|
91
|
-
|
92
|
-
});
|
95
|
+
return image.placeholderDataURL;
|
96
|
+
});
|
93
97
|
|
94
|
-
// Cleanup object URL on component destroy
|
95
|
-
onDestroy(() => {
|
96
|
-
|
97
|
-
});
|
98
|
+
// Cleanup object URL on component destroy
|
99
|
+
onDestroy(() => {
|
100
|
+
revokeObjectURL(lastBestImage?.[1]);
|
101
|
+
});
|
98
102
|
|
99
|
-
function revokeObjectURL(url: string | undefined) {
|
100
|
-
|
101
|
-
|
103
|
+
function revokeObjectURL(url: string | undefined) {
|
104
|
+
if (url && url.startsWith("blob:")) {
|
105
|
+
URL.revokeObjectURL(url);
|
106
|
+
}
|
102
107
|
}
|
103
|
-
}
|
104
108
|
|
105
|
-
const emptyPixelBlob = new Blob(
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
109
|
+
const emptyPixelBlob = new Blob(
|
110
|
+
[
|
111
|
+
Uint8Array.from(
|
112
|
+
atob(
|
113
|
+
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
|
114
|
+
),
|
115
|
+
(c) => c.charCodeAt(0),
|
110
116
|
),
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
{ type: "image/png" },
|
115
|
-
);
|
117
|
+
],
|
118
|
+
{ type: "image/png" },
|
119
|
+
);
|
116
120
|
</script>
|
117
121
|
|
118
122
|
<img
|
@@ -120,6 +124,8 @@ const emptyPixelBlob = new Blob(
|
|
120
124
|
width={dimensions.width}
|
121
125
|
height={dimensions.height}
|
122
126
|
alt={rest.alt}
|
123
|
-
onload={() => {
|
127
|
+
onload={() => {
|
128
|
+
waitingLazyLoading = false;
|
129
|
+
}}
|
124
130
|
{...rest}
|
125
131
|
/>
|
package/src/svelte/testing.ts
CHANGED
@@ -15,7 +15,7 @@ describe("Image", async () => {
|
|
15
15
|
render(Image, props, { account });
|
16
16
|
|
17
17
|
describe("initial rendering", () => {
|
18
|
-
it("should render
|
18
|
+
it("should render a blank placeholder while waiting for the coValue to load", async () => {
|
19
19
|
const { container } = renderWithAccount({
|
20
20
|
imageId: "co_zMTubMby3QiKDYnW9e2BEXW7Xaq",
|
21
21
|
alt: "test",
|
@@ -26,7 +26,23 @@ describe("Image", async () => {
|
|
26
26
|
expect(img!.getAttribute("width")).toBe(null);
|
27
27
|
expect(img!.getAttribute("height")).toBe(null);
|
28
28
|
expect(img!.alt).toBe("test");
|
29
|
-
expect(img!.src).toBe("");
|
29
|
+
expect(img!.src).toBe("data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==");
|
30
|
+
});
|
31
|
+
|
32
|
+
it("should render nothing if coValue is not found", async () => {
|
33
|
+
const { container } = renderWithAccount({
|
34
|
+
imageId: "co_zMTubMby3QiKDYnW9e2BEXW7Xaq",
|
35
|
+
alt: "test",
|
36
|
+
});
|
37
|
+
|
38
|
+
await waitFor(() => {
|
39
|
+
const img = container.querySelector("img");
|
40
|
+
expect(img).toBeDefined();
|
41
|
+
expect(img!.getAttribute("width")).toBe(null);
|
42
|
+
expect(img!.getAttribute("height")).toBe(null);
|
43
|
+
expect(img!.alt).toBe("test");
|
44
|
+
expect(img!.src).toBe("");
|
45
|
+
});
|
30
46
|
});
|
31
47
|
|
32
48
|
it("should render an empty image if the image is not loaded yet", async () => {
|
@@ -0,0 +1,61 @@
|
|
1
|
+
// @vitest-environment happy-dom
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
3
|
+
import {
|
4
|
+
createJazzTestAccount,
|
5
|
+
setupJazzTestSync,
|
6
|
+
MockConnectionStatus,
|
7
|
+
} from "../testing";
|
8
|
+
import { render, screen, waitFor } from "./testUtils";
|
9
|
+
import TestConnectionStatus from "./TestConnectionStatus.svelte";
|
10
|
+
|
11
|
+
describe("SyncConnectionStatus", () => {
|
12
|
+
beforeEach(async () => {
|
13
|
+
await setupJazzTestSync();
|
14
|
+
await createJazzTestAccount({
|
15
|
+
isCurrentActiveAccount: true,
|
16
|
+
});
|
17
|
+
});
|
18
|
+
|
19
|
+
afterEach(() => {
|
20
|
+
vi.clearAllMocks();
|
21
|
+
});
|
22
|
+
|
23
|
+
it("should return true by default in the test environment", async () => {
|
24
|
+
const { container } = render(TestConnectionStatus, {}, {
|
25
|
+
account: await createJazzTestAccount({
|
26
|
+
isCurrentActiveAccount: true,
|
27
|
+
}),
|
28
|
+
});
|
29
|
+
|
30
|
+
await waitFor(() => {
|
31
|
+
expect(screen.getByTestId("connected").textContent).toBe("true");
|
32
|
+
});
|
33
|
+
});
|
34
|
+
|
35
|
+
it("should handle updates", async () => {
|
36
|
+
const { container } = render(TestConnectionStatus, {}, {
|
37
|
+
account: await createJazzTestAccount({
|
38
|
+
isCurrentActiveAccount: true,
|
39
|
+
}),
|
40
|
+
});
|
41
|
+
|
42
|
+
// Initially should be connected
|
43
|
+
await waitFor(() => {
|
44
|
+
expect(screen.getByTestId("connected").textContent).toBe("true");
|
45
|
+
});
|
46
|
+
|
47
|
+
// Simulate disconnection
|
48
|
+
MockConnectionStatus.setIsConnected(false);
|
49
|
+
|
50
|
+
await waitFor(() => {
|
51
|
+
expect(screen.getByTestId("connected").textContent).toBe("false");
|
52
|
+
});
|
53
|
+
|
54
|
+
// Simulate reconnection
|
55
|
+
MockConnectionStatus.setIsConnected(true);
|
56
|
+
|
57
|
+
await waitFor(() => {
|
58
|
+
expect(screen.getByTestId("connected").textContent).toBe("true");
|
59
|
+
});
|
60
|
+
});
|
61
|
+
});
|
@@ -26,6 +26,8 @@ type PlatformSpecificAuthContext<Acc extends Account> = {
|
|
26
26
|
node: LocalNode;
|
27
27
|
logOut: () => Promise<void>;
|
28
28
|
done: () => void;
|
29
|
+
addConnectionListener: (listener: (connected: boolean) => void) => () => void;
|
30
|
+
connected: () => boolean;
|
29
31
|
};
|
30
32
|
|
31
33
|
type PlatformSpecificGuestContext = {
|
@@ -33,6 +35,8 @@ type PlatformSpecificGuestContext = {
|
|
33
35
|
node: LocalNode;
|
34
36
|
logOut: () => Promise<void>;
|
35
37
|
done: () => void;
|
38
|
+
addConnectionListener: (listener: (connected: boolean) => void) => () => void;
|
39
|
+
connected: () => boolean;
|
36
40
|
};
|
37
41
|
|
38
42
|
type PlatformSpecificContext<Acc extends Account> =
|
@@ -52,6 +56,8 @@ function getAnonymousFallback() {
|
|
52
56
|
logOut: async () => {},
|
53
57
|
isAuthenticated: false,
|
54
58
|
authenticate: async () => {},
|
59
|
+
addConnectionListener: () => () => {},
|
60
|
+
connected: () => false,
|
55
61
|
register: async () => {
|
56
62
|
throw new Error("Not implemented");
|
57
63
|
},
|
@@ -134,6 +140,8 @@ export class JazzContextManager<
|
|
134
140
|
authenticate: this.authenticate,
|
135
141
|
register: this.register,
|
136
142
|
logOut: this.logOut,
|
143
|
+
addConnectionListener: context.addConnectionListener,
|
144
|
+
connected: context.connected,
|
137
145
|
};
|
138
146
|
|
139
147
|
if (authProps?.credentials) {
|
@@ -1,9 +1,10 @@
|
|
1
1
|
import { type Account } from "../coValues/account.js";
|
2
|
-
import
|
2
|
+
import {
|
3
3
|
AnonymousJazzAgent,
|
4
4
|
CoValue,
|
5
5
|
ID,
|
6
6
|
RefEncoded,
|
7
|
+
SubscriptionScope,
|
7
8
|
} from "../internal.js";
|
8
9
|
import {
|
9
10
|
accessChildById,
|
@@ -26,9 +27,28 @@ export class Ref<out V extends CoValue> {
|
|
26
27
|
async load(): Promise<V | null> {
|
27
28
|
const subscriptionScope = getSubscriptionScope(this.parent);
|
28
29
|
|
29
|
-
|
30
|
+
let node: SubscriptionScope<CoValue> | undefined | null;
|
30
31
|
|
31
|
-
|
32
|
+
/**
|
33
|
+
* If the parent subscription scope is closed, we can't use it
|
34
|
+
* to subscribe to the child id, so we create a detached subscription scope
|
35
|
+
* that is going to be destroyed immediately after the load
|
36
|
+
*/
|
37
|
+
if (subscriptionScope.closed) {
|
38
|
+
node = new SubscriptionScope<CoValue>(
|
39
|
+
subscriptionScope.node,
|
40
|
+
true,
|
41
|
+
this.id,
|
42
|
+
this.schema,
|
43
|
+
subscriptionScope.skipRetry,
|
44
|
+
subscriptionScope.bestEffortResolution,
|
45
|
+
subscriptionScope.unstable_branch,
|
46
|
+
);
|
47
|
+
} else {
|
48
|
+
subscriptionScope.subscribeToId(this.id, this.schema);
|
49
|
+
|
50
|
+
node = subscriptionScope.childNodes.get(this.id);
|
51
|
+
}
|
32
52
|
|
33
53
|
if (!node) {
|
34
54
|
return null;
|
@@ -51,6 +71,10 @@ export class Ref<out V extends CoValue> {
|
|
51
71
|
unsubscribe();
|
52
72
|
resolve(null);
|
53
73
|
}
|
74
|
+
|
75
|
+
if (subscriptionScope.closed) {
|
76
|
+
node.destroy();
|
77
|
+
}
|
54
78
|
});
|
55
79
|
});
|
56
80
|
}
|
@@ -41,6 +41,20 @@ export class CoValueCoreSubscription {
|
|
41
41
|
this.initializeSubscription();
|
42
42
|
}
|
43
43
|
|
44
|
+
/**
|
45
|
+
* Rehydrates the subscription by resetting the unsubscribed flag and initializing the subscription again
|
46
|
+
*/
|
47
|
+
pullValue() {
|
48
|
+
if (!this.unsubscribed) {
|
49
|
+
return;
|
50
|
+
}
|
51
|
+
|
52
|
+
// Reset the unsubscribed flag so we can initialize the subscription again
|
53
|
+
this.unsubscribed = false;
|
54
|
+
this.initializeSubscription();
|
55
|
+
this.unsubscribe();
|
56
|
+
}
|
57
|
+
|
44
58
|
/**
|
45
59
|
* Main entry point for subscription initialization.
|
46
60
|
* Determines the subscription strategy based on current availability and branch requirements.
|
@@ -45,6 +45,7 @@ export class SubscriptionScope<D extends CoValue> {
|
|
45
45
|
totalValidTransactions = 0;
|
46
46
|
migrated = false;
|
47
47
|
migrating = false;
|
48
|
+
closed = false;
|
48
49
|
|
49
50
|
silenceUpdates = false;
|
50
51
|
|
@@ -69,7 +70,6 @@ export class SubscriptionScope<D extends CoValue> {
|
|
69
70
|
|
70
71
|
if (skipRetry && value === "unavailable") {
|
71
72
|
this.handleUpdate(value);
|
72
|
-
this.destroy();
|
73
73
|
return;
|
74
74
|
}
|
75
75
|
|
@@ -79,7 +79,11 @@ export class SubscriptionScope<D extends CoValue> {
|
|
79
79
|
// - Run the migration only once
|
80
80
|
// - Skip all the updates until the migration is done
|
81
81
|
// - Trigger handleUpdate only with the final value
|
82
|
-
if (
|
82
|
+
if (
|
83
|
+
!this.migrated &&
|
84
|
+
value !== "unavailable" &&
|
85
|
+
!value.core.verified.isStreaming()
|
86
|
+
) {
|
83
87
|
if (this.migrating) {
|
84
88
|
return;
|
85
89
|
}
|
@@ -398,8 +402,51 @@ export class SubscriptionScope<D extends CoValue> {
|
|
398
402
|
);
|
399
403
|
}
|
400
404
|
|
405
|
+
/**
|
406
|
+
* Checks if the currently unloaded value has got some updates
|
407
|
+
*
|
408
|
+
* Used to make the autoload work on closed subscription scopes
|
409
|
+
*/
|
410
|
+
pullValue(listener: (value: SubscriptionValue<D, any>) => void) {
|
411
|
+
if (!this.closed) {
|
412
|
+
throw new Error("Cannot pull a non-closed subscription scope");
|
413
|
+
}
|
414
|
+
|
415
|
+
if (this.value.type === "loaded") {
|
416
|
+
return;
|
417
|
+
}
|
418
|
+
|
419
|
+
// Try to pull the value from the subscription
|
420
|
+
// into the SubscriptionScope update flow
|
421
|
+
this.subscription.pullValue();
|
422
|
+
|
423
|
+
// Check if the value is now available
|
424
|
+
const value = this.getCurrentValue();
|
425
|
+
|
426
|
+
// If the value is available, trigger the listener
|
427
|
+
if (value) {
|
428
|
+
listener({
|
429
|
+
type: "loaded",
|
430
|
+
value,
|
431
|
+
id: this.id,
|
432
|
+
});
|
433
|
+
}
|
434
|
+
}
|
435
|
+
|
401
436
|
subscribeToId(id: string, descriptor: RefEncoded<any>) {
|
402
437
|
if (this.isSubscribedToId(id)) {
|
438
|
+
if (!this.closed) {
|
439
|
+
return;
|
440
|
+
}
|
441
|
+
|
442
|
+
const child = this.childNodes.get(id);
|
443
|
+
|
444
|
+
// If the subscription is closed, check if we missed the value
|
445
|
+
// load event
|
446
|
+
if (child) {
|
447
|
+
child.pullValue((value) => this.handleChildUpdate(id, value));
|
448
|
+
}
|
449
|
+
|
403
450
|
return;
|
404
451
|
}
|
405
452
|
|
@@ -426,6 +473,14 @@ export class SubscriptionScope<D extends CoValue> {
|
|
426
473
|
this.childNodes.set(id, child);
|
427
474
|
child.setListener((value) => this.handleChildUpdate(id, value));
|
428
475
|
|
476
|
+
/**
|
477
|
+
* If the current subscription scope is closed, spawn
|
478
|
+
* child nodes only to load in-memory values
|
479
|
+
*/
|
480
|
+
if (this.closed) {
|
481
|
+
child.destroy();
|
482
|
+
}
|
483
|
+
|
429
484
|
this.silenceUpdates = false;
|
430
485
|
}
|
431
486
|
|
@@ -676,9 +731,19 @@ export class SubscriptionScope<D extends CoValue> {
|
|
676
731
|
);
|
677
732
|
this.childNodes.set(id, child);
|
678
733
|
child.setListener((value) => this.handleChildUpdate(id, value, key));
|
734
|
+
|
735
|
+
/**
|
736
|
+
* If the current subscription scope is closed, spawn
|
737
|
+
* child nodes only to load in-memory values
|
738
|
+
*/
|
739
|
+
if (this.closed) {
|
740
|
+
child.destroy();
|
741
|
+
}
|
679
742
|
}
|
680
743
|
|
681
744
|
destroy() {
|
745
|
+
this.closed = true;
|
746
|
+
|
682
747
|
this.subscription.unsubscribe();
|
683
748
|
this.subscribers.clear();
|
684
749
|
this.childNodes.forEach((child) => child.destroy());
|
@@ -24,6 +24,8 @@ export function getSubscriptionScope<D extends CoValue>(value: D) {
|
|
24
24
|
configurable: false,
|
25
25
|
});
|
26
26
|
|
27
|
+
newSubscriptionScope.destroy();
|
28
|
+
|
27
29
|
return newSubscriptionScope;
|
28
30
|
}
|
29
31
|
|
@@ -42,8 +44,14 @@ export function accessChildByKey<D extends CoValue>(
|
|
42
44
|
) {
|
43
45
|
const subscriptionScope = getSubscriptionScope(parent);
|
44
46
|
|
47
|
+
const node = subscriptionScope.childNodes.get(childId);
|
48
|
+
|
45
49
|
if (!subscriptionScope.isSubscribedToId(childId)) {
|
46
50
|
subscriptionScope.subscribeToKey(key);
|
51
|
+
} else if (node && node.closed) {
|
52
|
+
node.pullValue((value) =>
|
53
|
+
subscriptionScope.handleChildUpdate(childId, value),
|
54
|
+
);
|
47
55
|
}
|
48
56
|
|
49
57
|
const value = subscriptionScope.childValues.get(childId);
|