jazz-tools 0.18.16 → 0.18.17

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 (36) hide show
  1. package/.svelte-kit/__package__/media/image.svelte +104 -98
  2. package/.svelte-kit/__package__/media/image.svelte.d.ts.map +1 -1
  3. package/.svelte-kit/__package__/tests/media/image.svelte.test.js +16 -2
  4. package/.turbo/turbo-build.log +42 -42
  5. package/CHANGELOG.md +11 -0
  6. package/dist/{chunk-GRN6OAUX.js → chunk-OTWWOZMB.js} +73 -4
  7. package/dist/chunk-OTWWOZMB.js.map +1 -0
  8. package/dist/index.js +1 -1
  9. package/dist/react/index.js +2 -0
  10. package/dist/react/index.js.map +1 -1
  11. package/dist/react/media/image.d.ts.map +1 -1
  12. package/dist/react-native-core/index.js +3 -1
  13. package/dist/react-native-core/index.js.map +1 -1
  14. package/dist/react-native-core/media/image.d.ts.map +1 -1
  15. package/dist/svelte/media/image.svelte +104 -98
  16. package/dist/svelte/media/image.svelte.d.ts.map +1 -1
  17. package/dist/svelte/tests/media/image.svelte.test.js +16 -2
  18. package/dist/testing.js +1 -1
  19. package/dist/tools/implementation/refs.d.ts +1 -1
  20. package/dist/tools/implementation/refs.d.ts.map +1 -1
  21. package/dist/tools/subscribe/CoValueCoreSubscription.d.ts +4 -0
  22. package/dist/tools/subscribe/CoValueCoreSubscription.d.ts.map +1 -1
  23. package/dist/tools/subscribe/SubscriptionScope.d.ts +7 -0
  24. package/dist/tools/subscribe/SubscriptionScope.d.ts.map +1 -1
  25. package/dist/tools/subscribe/index.d.ts.map +1 -1
  26. package/package.json +4 -4
  27. package/src/react/media/image.tsx +2 -0
  28. package/src/react/tests/media/image.test.tsx +20 -2
  29. package/src/react-native-core/media/image.tsx +4 -1
  30. package/src/svelte/media/image.svelte +104 -98
  31. package/src/svelte/tests/media/image.svelte.test.ts +18 -2
  32. package/src/tools/implementation/refs.ts +27 -3
  33. package/src/tools/subscribe/CoValueCoreSubscription.ts +14 -0
  34. package/src/tools/subscribe/SubscriptionScope.ts +62 -1
  35. package/src/tools/subscribe/index.ts +8 -0
  36. package/dist/chunk-GRN6OAUX.js.map +0 -1
@@ -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
  />
@@ -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 () => {
@@ -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
 
@@ -398,8 +398,51 @@ export class SubscriptionScope<D extends CoValue> {
398
398
  );
399
399
  }
400
400
 
401
+ /**
402
+ * Checks if the currently unloaded value has got some updates
403
+ *
404
+ * Used to make the autoload work on closed subscription scopes
405
+ */
406
+ pullValue(listener: (value: SubscriptionValue<D, any>) => void) {
407
+ if (!this.closed) {
408
+ throw new Error("Cannot pull a non-closed subscription scope");
409
+ }
410
+
411
+ if (this.value.type === "loaded") {
412
+ return;
413
+ }
414
+
415
+ // Try to pull the value from the subscription
416
+ // into the SubscriptionScope update flow
417
+ this.subscription.pullValue();
418
+
419
+ // Check if the value is now available
420
+ const value = this.getCurrentValue();
421
+
422
+ // If the value is available, trigger the listener
423
+ if (value) {
424
+ listener({
425
+ type: "loaded",
426
+ value,
427
+ id: this.id,
428
+ });
429
+ }
430
+ }
431
+
401
432
  subscribeToId(id: string, descriptor: RefEncoded<any>) {
402
433
  if (this.isSubscribedToId(id)) {
434
+ if (!this.closed) {
435
+ return;
436
+ }
437
+
438
+ const child = this.childNodes.get(id);
439
+
440
+ // If the subscription is closed, check if we missed the value
441
+ // load event
442
+ if (child) {
443
+ child.pullValue((value) => this.handleChildUpdate(id, value));
444
+ }
445
+
403
446
  return;
404
447
  }
405
448
 
@@ -426,6 +469,14 @@ export class SubscriptionScope<D extends CoValue> {
426
469
  this.childNodes.set(id, child);
427
470
  child.setListener((value) => this.handleChildUpdate(id, value));
428
471
 
472
+ /**
473
+ * If the current subscription scope is closed, spawn
474
+ * child nodes only to load in-memory values
475
+ */
476
+ if (this.closed) {
477
+ child.destroy();
478
+ }
479
+
429
480
  this.silenceUpdates = false;
430
481
  }
431
482
 
@@ -676,9 +727,19 @@ export class SubscriptionScope<D extends CoValue> {
676
727
  );
677
728
  this.childNodes.set(id, child);
678
729
  child.setListener((value) => this.handleChildUpdate(id, value, key));
730
+
731
+ /**
732
+ * If the current subscription scope is closed, spawn
733
+ * child nodes only to load in-memory values
734
+ */
735
+ if (this.closed) {
736
+ child.destroy();
737
+ }
679
738
  }
680
739
 
681
740
  destroy() {
741
+ this.closed = true;
742
+
682
743
  this.subscription.unsubscribe();
683
744
  this.subscribers.clear();
684
745
  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);