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.
- 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__/tests/media/image.svelte.test.js +16 -2
- package/.turbo/turbo-build.log +42 -42
- package/CHANGELOG.md +11 -0
- package/dist/{chunk-GRN6OAUX.js → chunk-OTWWOZMB.js} +73 -4
- package/dist/chunk-OTWWOZMB.js.map +1 -0
- package/dist/index.js +1 -1
- package/dist/react/index.js +2 -0
- package/dist/react/index.js.map +1 -1
- package/dist/react/media/image.d.ts.map +1 -1
- package/dist/react-native-core/index.js +3 -1
- package/dist/react-native-core/index.js.map +1 -1
- package/dist/react-native-core/media/image.d.ts.map +1 -1
- package/dist/svelte/media/image.svelte +104 -98
- package/dist/svelte/media/image.svelte.d.ts.map +1 -1
- package/dist/svelte/tests/media/image.svelte.test.js +16 -2
- package/dist/testing.js +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/package.json +4 -4
- package/src/react/media/image.tsx +2 -0
- package/src/react/tests/media/image.test.tsx +20 -2
- package/src/react-native-core/media/image.tsx +4 -1
- package/src/svelte/media/image.svelte +104 -98
- package/src/svelte/tests/media/image.svelte.test.ts +18 -2
- package/src/tools/implementation/refs.ts +27 -3
- package/src/tools/subscribe/CoValueCoreSubscription.ts +14 -0
- package/src/tools/subscribe/SubscriptionScope.ts +62 -1
- package/src/tools/subscribe/index.ts +8 -0
- 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
|
-
|
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
|
/>
|
@@ -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 () => {
|
@@ -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
|
|
@@ -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);
|