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.
Files changed (112) hide show
  1. package/.svelte-kit/__package__/jazz.class.svelte.d.ts +14 -0
  2. package/.svelte-kit/__package__/jazz.class.svelte.d.ts.map +1 -1
  3. package/.svelte-kit/__package__/jazz.class.svelte.js +37 -0
  4. package/.svelte-kit/__package__/media/image.svelte +104 -98
  5. package/.svelte-kit/__package__/media/image.svelte.d.ts.map +1 -1
  6. package/.svelte-kit/__package__/testing.d.ts +1 -1
  7. package/.svelte-kit/__package__/testing.d.ts.map +1 -1
  8. package/.svelte-kit/__package__/testing.js +1 -1
  9. package/.svelte-kit/__package__/tests/TestConnectionStatus.svelte +8 -0
  10. package/.svelte-kit/__package__/tests/TestConnectionStatus.svelte.d.ts +27 -0
  11. package/.svelte-kit/__package__/tests/TestConnectionStatus.svelte.d.ts.map +1 -0
  12. package/.svelte-kit/__package__/tests/media/image.svelte.test.js +16 -2
  13. package/.svelte-kit/__package__/tests/sync-connection-status.svelte.test.d.ts +2 -0
  14. package/.svelte-kit/__package__/tests/sync-connection-status.svelte.test.d.ts.map +1 -0
  15. package/.svelte-kit/__package__/tests/sync-connection-status.svelte.test.js +47 -0
  16. package/.turbo/turbo-build.log +52 -52
  17. package/CHANGELOG.md +27 -0
  18. package/dist/browser/BrowserContextManager.d.ts +4 -0
  19. package/dist/browser/BrowserContextManager.d.ts.map +1 -1
  20. package/dist/browser/createBrowserContext.d.ts +4 -0
  21. package/dist/browser/createBrowserContext.d.ts.map +1 -1
  22. package/dist/browser/index.js +36 -4
  23. package/dist/browser/index.js.map +1 -1
  24. package/dist/{chunk-GRN6OAUX.js → chunk-FHRKDKDY.js} +80 -6
  25. package/dist/chunk-FHRKDKDY.js.map +1 -0
  26. package/dist/index.js +1 -1
  27. package/dist/react/hooks.d.ts +1 -1
  28. package/dist/react/hooks.d.ts.map +1 -1
  29. package/dist/react/index.d.ts +1 -1
  30. package/dist/react/index.d.ts.map +1 -1
  31. package/dist/react/index.js +6 -2
  32. package/dist/react/index.js.map +1 -1
  33. package/dist/react/media/image.d.ts.map +1 -1
  34. package/dist/react-core/hooks.d.ts +26 -0
  35. package/dist/react-core/hooks.d.ts.map +1 -1
  36. package/dist/react-core/index.js +16 -1
  37. package/dist/react-core/index.js.map +1 -1
  38. package/dist/react-core/testing.d.ts +1 -1
  39. package/dist/react-core/testing.d.ts.map +1 -1
  40. package/dist/react-core/testing.js +3 -1
  41. package/dist/react-core/testing.js.map +1 -1
  42. package/dist/react-core/tests/useSyncConnectionStatus.test.d.ts +2 -0
  43. package/dist/react-core/tests/useSyncConnectionStatus.test.d.ts.map +1 -0
  44. package/dist/react-native-core/ReactNativeContextManager.d.ts +4 -0
  45. package/dist/react-native-core/ReactNativeContextManager.d.ts.map +1 -1
  46. package/dist/react-native-core/hooks.d.ts +1 -1
  47. package/dist/react-native-core/hooks.d.ts.map +1 -1
  48. package/dist/react-native-core/index.js +41 -7
  49. package/dist/react-native-core/index.js.map +1 -1
  50. package/dist/react-native-core/media/image.d.ts.map +1 -1
  51. package/dist/react-native-core/platform.d.ts +4 -0
  52. package/dist/react-native-core/platform.d.ts.map +1 -1
  53. package/dist/svelte/jazz.class.svelte.d.ts +14 -0
  54. package/dist/svelte/jazz.class.svelte.d.ts.map +1 -1
  55. package/dist/svelte/jazz.class.svelte.js +37 -0
  56. package/dist/svelte/media/image.svelte +104 -98
  57. package/dist/svelte/media/image.svelte.d.ts.map +1 -1
  58. package/dist/svelte/testing.d.ts +1 -1
  59. package/dist/svelte/testing.d.ts.map +1 -1
  60. package/dist/svelte/testing.js +1 -1
  61. package/dist/svelte/tests/TestConnectionStatus.svelte +8 -0
  62. package/dist/svelte/tests/TestConnectionStatus.svelte.d.ts +27 -0
  63. package/dist/svelte/tests/TestConnectionStatus.svelte.d.ts.map +1 -0
  64. package/dist/svelte/tests/media/image.svelte.test.js +16 -2
  65. package/dist/svelte/tests/sync-connection-status.svelte.test.d.ts +2 -0
  66. package/dist/svelte/tests/sync-connection-status.svelte.test.d.ts.map +1 -0
  67. package/dist/svelte/tests/sync-connection-status.svelte.test.js +47 -0
  68. package/dist/testing.js +34 -4
  69. package/dist/testing.js.map +1 -1
  70. package/dist/tools/implementation/ContextManager.d.ts +4 -0
  71. package/dist/tools/implementation/ContextManager.d.ts.map +1 -1
  72. package/dist/tools/implementation/refs.d.ts +1 -1
  73. package/dist/tools/implementation/refs.d.ts.map +1 -1
  74. package/dist/tools/subscribe/CoValueCoreSubscription.d.ts +4 -0
  75. package/dist/tools/subscribe/CoValueCoreSubscription.d.ts.map +1 -1
  76. package/dist/tools/subscribe/SubscriptionScope.d.ts +7 -0
  77. package/dist/tools/subscribe/SubscriptionScope.d.ts.map +1 -1
  78. package/dist/tools/subscribe/index.d.ts.map +1 -1
  79. package/dist/tools/testing.d.ts +8 -0
  80. package/dist/tools/testing.d.ts.map +1 -1
  81. package/dist/tools/types.d.ts +4 -0
  82. package/dist/tools/types.d.ts.map +1 -1
  83. package/package.json +4 -4
  84. package/src/browser/createBrowserContext.ts +34 -4
  85. package/src/react/hooks.tsx +1 -0
  86. package/src/react/index.ts +1 -0
  87. package/src/react/media/image.tsx +2 -0
  88. package/src/react/tests/media/image.test.tsx +20 -2
  89. package/src/react-core/hooks.ts +42 -0
  90. package/src/react-core/testing.tsx +1 -0
  91. package/src/react-core/tests/useAccountWithSelector.test.ts +98 -2
  92. package/src/react-core/tests/useSyncConnectionStatus.test.ts +48 -0
  93. package/src/react-native-core/hooks.tsx +1 -0
  94. package/src/react-native-core/media/image.tsx +4 -1
  95. package/src/react-native-core/platform.ts +32 -4
  96. package/src/svelte/jazz.class.svelte.ts +44 -0
  97. package/src/svelte/media/image.svelte +104 -98
  98. package/src/svelte/testing.ts +1 -0
  99. package/src/svelte/tests/TestConnectionStatus.svelte +8 -0
  100. package/src/svelte/tests/media/image.svelte.test.ts +18 -2
  101. package/src/svelte/tests/sync-connection-status.svelte.test.ts +61 -0
  102. package/src/tools/implementation/ContextManager.ts +8 -0
  103. package/src/tools/implementation/refs.ts +27 -3
  104. package/src/tools/subscribe/CoValueCoreSubscription.ts +14 -0
  105. package/src/tools/subscribe/SubscriptionScope.ts +67 -2
  106. package/src/tools/subscribe/index.ts +8 -0
  107. package/src/tools/testing.ts +29 -0
  108. package/src/tools/tests/ContextManager.test.ts +2 -2
  109. package/src/tools/tests/coMap.test.ts +42 -0
  110. package/src/tools/tests/subscribe.test.ts +1 -4
  111. package/src/tools/types.ts +4 -0
  112. 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
- * 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];
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
- // 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
- };
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
- // 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
- };
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
- // In all other cases, use the property value:
59
- return {
60
- width: width === "original" ? originalWidth : width,
61
- height: height === "original" ? originalHeight : height,
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
- const src = $derived.by(() => {
66
- if (waitingLazyLoading) {
67
- return lazyPlaceholder;
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 image = imageState.current;
71
- if (!image) return undefined;
65
+ const src = $derived.by(() => {
66
+ if (waitingLazyLoading) {
67
+ return lazyPlaceholder;
68
+ }
72
69
 
73
- const bestImage = highestResAvailable(
74
- image,
75
- dimensions.width || dimensions.height || 9999,
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
- if (!bestImage) return image.placeholderDataURL;
80
- if (lastBestImage?.[0] === bestImage.image.$jazz.id) return lastBestImage?.[1];
74
+ if (!image) return undefined;
81
75
 
82
- const blob = bestImage.image.toBlob();
76
+ const bestImage = highestResAvailable(
77
+ image,
78
+ dimensions.width || dimensions.height || 9999,
79
+ dimensions.height || dimensions.width || 9999,
80
+ );
83
81
 
84
- if (blob) {
85
- const url = URL.createObjectURL(blob);
86
- revokeObjectURL(lastBestImage?.[1]);
87
- lastBestImage = [bestImage.image.$jazz.id, url];
88
- return url;
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
- return image.placeholderDataURL;
92
- });
95
+ return image.placeholderDataURL;
96
+ });
93
97
 
94
- // Cleanup object URL on component destroy
95
- onDestroy(() => {
96
- revokeObjectURL(lastBestImage?.[1]);
97
- });
98
+ // Cleanup object URL on component destroy
99
+ onDestroy(() => {
100
+ revokeObjectURL(lastBestImage?.[1]);
101
+ });
98
102
 
99
- function revokeObjectURL(url: string | undefined) {
100
- if (url && url.startsWith("blob:")) {
101
- URL.revokeObjectURL(url);
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
- Uint8Array.from(
108
- atob(
109
- "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==",
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
- (c) => c.charCodeAt(0),
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={() => {waitingLazyLoading = false}}
127
+ onload={() => {
128
+ waitingLazyLoading = false;
129
+ }}
124
130
  {...rest}
125
131
  />
@@ -39,4 +39,5 @@ export {
39
39
  linkAccounts,
40
40
  setActiveAccount,
41
41
  setupJazzTestSync,
42
+ MockConnectionStatus,
42
43
  } from "jazz-tools/testing";
@@ -0,0 +1,8 @@
1
+ <script>
2
+ import { SyncConnectionStatus } from "../jazz.class.svelte";
3
+ const connectionStatus = new SyncConnectionStatus();
4
+ </script>
5
+
6
+ <div>
7
+ <div data-testid="connected">{connectionStatus.current ? "true" : "false"}</div>
8
+ </div>
@@ -15,7 +15,7 @@ describe("Image", async () => {
15
15
  render(Image, props, { account });
16
16
 
17
17
  describe("initial rendering", () => {
18
- it("should render nothing if coValue is not found", async () => {
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 type {
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
- subscriptionScope.subscribeToId(this.id, this.schema);
30
+ let node: SubscriptionScope<CoValue> | undefined | null;
30
31
 
31
- const node = subscriptionScope.childNodes.get(this.id);
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 (!this.migrated && value !== "unavailable") {
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);