jazz-tools 0.18.15 → 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 +48 -38
- package/CHANGELOG.md +20 -0
- package/dist/better-auth/database-adapter/index.d.ts +50 -0
- package/dist/better-auth/database-adapter/index.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/index.js +920 -0
- package/dist/better-auth/database-adapter/index.js.map +1 -0
- package/dist/better-auth/database-adapter/repository/account.d.ts +24 -0
- package/dist/better-auth/database-adapter/repository/account.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/repository/generic.d.ts +45 -0
- package/dist/better-auth/database-adapter/repository/generic.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/repository/index.d.ts +6 -0
- package/dist/better-auth/database-adapter/repository/index.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/repository/session.d.ts +29 -0
- package/dist/better-auth/database-adapter/repository/session.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/repository/user.d.ts +30 -0
- package/dist/better-auth/database-adapter/repository/user.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/repository/verification.d.ts +18 -0
- package/dist/better-auth/database-adapter/repository/verification.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/schema.d.ts +27 -0
- package/dist/better-auth/database-adapter/schema.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/tests/index.test.d.ts +2 -0
- package/dist/better-auth/database-adapter/tests/index.test.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/tests/repository/account.test.d.ts +2 -0
- package/dist/better-auth/database-adapter/tests/repository/account.test.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/tests/repository/generic.test.d.ts +2 -0
- package/dist/better-auth/database-adapter/tests/repository/generic.test.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/tests/repository/session.test.d.ts +2 -0
- package/dist/better-auth/database-adapter/tests/repository/session.test.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/tests/repository/user.test.d.ts +2 -0
- package/dist/better-auth/database-adapter/tests/repository/user.test.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/tests/repository/verification.test.d.ts +2 -0
- package/dist/better-auth/database-adapter/tests/repository/verification.test.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/tests/sync-utils.d.ts +16 -0
- package/dist/better-auth/database-adapter/tests/sync-utils.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/tests/utils.test.d.ts +2 -0
- package/dist/better-auth/database-adapter/tests/utils.test.d.ts.map +1 -0
- package/dist/better-auth/database-adapter/utils.d.ts +16 -0
- package/dist/better-auth/database-adapter/utils.d.ts.map +1 -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/jazz-tools-0.18.6.tgz +0 -0
- package/package.json +10 -5
- package/src/better-auth/database-adapter/index.ts +228 -0
- package/src/better-auth/database-adapter/repository/account.ts +131 -0
- package/src/better-auth/database-adapter/repository/generic.ts +297 -0
- package/src/better-auth/database-adapter/repository/index.ts +5 -0
- package/src/better-auth/database-adapter/repository/session.ts +190 -0
- package/src/better-auth/database-adapter/repository/user.ts +158 -0
- package/src/better-auth/database-adapter/repository/verification.ts +37 -0
- package/src/better-auth/database-adapter/schema.ts +222 -0
- package/src/better-auth/database-adapter/tests/index.test.ts +690 -0
- package/src/better-auth/database-adapter/tests/repository/account.test.ts +149 -0
- package/src/better-auth/database-adapter/tests/repository/generic.test.ts +183 -0
- package/src/better-auth/database-adapter/tests/repository/session.test.ts +419 -0
- package/src/better-auth/database-adapter/tests/repository/user.test.ts +673 -0
- package/src/better-auth/database-adapter/tests/repository/verification.test.ts +101 -0
- package/src/better-auth/database-adapter/tests/sync-utils.ts +127 -0
- package/src/better-auth/database-adapter/tests/utils.test.ts +787 -0
- package/src/better-auth/database-adapter/utils.ts +178 -0
- 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/tsup.config.ts +7 -0
- package/dist/chunk-GRN6OAUX.js.map +0 -1
@@ -0,0 +1,178 @@
|
|
1
|
+
import { CleanedWhere } from "better-auth/adapters";
|
2
|
+
|
3
|
+
export function filterListByWhere<T>(
|
4
|
+
data: T[],
|
5
|
+
where: CleanedWhere[] | undefined,
|
6
|
+
): T[] {
|
7
|
+
if (!Array.isArray(data)) {
|
8
|
+
throw new Error("Expected data to be an array");
|
9
|
+
}
|
10
|
+
|
11
|
+
if (where === undefined) {
|
12
|
+
return data;
|
13
|
+
}
|
14
|
+
|
15
|
+
if (!Array.isArray(where)) {
|
16
|
+
throw new Error("Expected where to be an array");
|
17
|
+
}
|
18
|
+
|
19
|
+
// Helper to evaluate a single condition
|
20
|
+
function evaluateCondition(item: any, condition: CleanedWhere): boolean {
|
21
|
+
const { field, operator, value } = condition;
|
22
|
+
const itemValue = field === "id" ? item.$jazz.id : item[field];
|
23
|
+
|
24
|
+
switch (operator) {
|
25
|
+
case "eq":
|
26
|
+
return itemValue === value;
|
27
|
+
case "ne":
|
28
|
+
return itemValue !== value;
|
29
|
+
case "lt":
|
30
|
+
return value !== null && itemValue < value;
|
31
|
+
case "lte":
|
32
|
+
return value !== null && itemValue <= value;
|
33
|
+
case "gt":
|
34
|
+
return value !== null && itemValue > value;
|
35
|
+
case "gte":
|
36
|
+
return value !== null && itemValue >= value;
|
37
|
+
case "in":
|
38
|
+
return Array.isArray(value)
|
39
|
+
? (value as (string | number | boolean | Date)[]).includes(itemValue)
|
40
|
+
: false;
|
41
|
+
case "contains":
|
42
|
+
return typeof itemValue === "string" && typeof value === "string"
|
43
|
+
? itemValue.includes(value)
|
44
|
+
: false;
|
45
|
+
case "starts_with":
|
46
|
+
return typeof itemValue === "string" && typeof value === "string"
|
47
|
+
? itemValue.startsWith(value)
|
48
|
+
: false;
|
49
|
+
case "ends_with":
|
50
|
+
return typeof itemValue === "string" && typeof value === "string"
|
51
|
+
? itemValue.endsWith(value)
|
52
|
+
: false;
|
53
|
+
default:
|
54
|
+
throw new Error(`Unsupported operator: ${operator}`);
|
55
|
+
}
|
56
|
+
}
|
57
|
+
|
58
|
+
// Group conditions by connector (AND/OR)
|
59
|
+
// If no connector, default to AND between all
|
60
|
+
return data.filter((item) => {
|
61
|
+
let result: boolean = true;
|
62
|
+
for (let i = 0; i < where.length; i++) {
|
63
|
+
const condition = where[i]!;
|
64
|
+
const matches = evaluateCondition(item, condition);
|
65
|
+
if (i === 0) {
|
66
|
+
result = matches;
|
67
|
+
} else {
|
68
|
+
const connector = condition.connector || "AND";
|
69
|
+
if (connector === "AND") {
|
70
|
+
result = result && matches;
|
71
|
+
} else if (connector === "OR") {
|
72
|
+
result = result || matches;
|
73
|
+
} else {
|
74
|
+
throw new Error(`Unsupported connector: ${connector}`);
|
75
|
+
}
|
76
|
+
}
|
77
|
+
}
|
78
|
+
return result;
|
79
|
+
});
|
80
|
+
}
|
81
|
+
|
82
|
+
export function sortListByField<T extends Record<string, any> | null>(
|
83
|
+
data: T[],
|
84
|
+
sort?: { field: string; direction: "asc" | "desc" },
|
85
|
+
): T[] {
|
86
|
+
if (!sort) {
|
87
|
+
return data;
|
88
|
+
}
|
89
|
+
|
90
|
+
const { field, direction } = sort;
|
91
|
+
|
92
|
+
data.sort((a, b) => {
|
93
|
+
if (a === null || b === null) {
|
94
|
+
return 0;
|
95
|
+
}
|
96
|
+
|
97
|
+
if (typeof a[field] === "string" && typeof b[field] === "string") {
|
98
|
+
return direction === "asc"
|
99
|
+
? a[field].localeCompare(b[field])
|
100
|
+
: b[field].localeCompare(a[field]);
|
101
|
+
}
|
102
|
+
|
103
|
+
return direction === "asc" ? a[field] - b[field] : b[field] - a[field];
|
104
|
+
});
|
105
|
+
|
106
|
+
return data;
|
107
|
+
}
|
108
|
+
|
109
|
+
export function paginateList<T>(
|
110
|
+
data: T[],
|
111
|
+
limit: number | undefined,
|
112
|
+
offset: number | undefined,
|
113
|
+
): T[] {
|
114
|
+
if (offset === undefined && limit === undefined) {
|
115
|
+
return data;
|
116
|
+
}
|
117
|
+
|
118
|
+
if (limit === 0) {
|
119
|
+
return [];
|
120
|
+
}
|
121
|
+
|
122
|
+
let start = offset ?? 0;
|
123
|
+
if (start < 0) {
|
124
|
+
start = 0;
|
125
|
+
}
|
126
|
+
|
127
|
+
const end = limit ? start + limit : undefined;
|
128
|
+
return data.slice(start, end);
|
129
|
+
}
|
130
|
+
|
131
|
+
function isWhereByField(field: string, where: CleanedWhere): boolean {
|
132
|
+
return (
|
133
|
+
where.field === field &&
|
134
|
+
where.operator === "eq" &&
|
135
|
+
where.connector === "AND"
|
136
|
+
);
|
137
|
+
}
|
138
|
+
|
139
|
+
export function isWhereBySingleField<T extends string>(
|
140
|
+
field: T,
|
141
|
+
where: CleanedWhere[] | undefined,
|
142
|
+
): where is [{ field: T; operator: "eq"; value: string; connector: "AND" }] {
|
143
|
+
if (where === undefined || where.length !== 1) {
|
144
|
+
return false;
|
145
|
+
}
|
146
|
+
|
147
|
+
const [cond] = where;
|
148
|
+
if (!cond) {
|
149
|
+
return false;
|
150
|
+
}
|
151
|
+
|
152
|
+
return isWhereByField(field, cond);
|
153
|
+
}
|
154
|
+
|
155
|
+
export function containWhereByField<T extends string>(
|
156
|
+
field: T,
|
157
|
+
where: CleanedWhere[] | undefined,
|
158
|
+
): boolean {
|
159
|
+
if (where === undefined) {
|
160
|
+
return false;
|
161
|
+
}
|
162
|
+
|
163
|
+
return where.some((cond) => isWhereByField(field, cond));
|
164
|
+
}
|
165
|
+
|
166
|
+
export function extractWhereByField<T extends string>(
|
167
|
+
field: T,
|
168
|
+
where: CleanedWhere[] | undefined,
|
169
|
+
): [CleanedWhere | undefined, CleanedWhere[]] {
|
170
|
+
if (where === undefined) {
|
171
|
+
return [undefined, []];
|
172
|
+
}
|
173
|
+
|
174
|
+
return [
|
175
|
+
where.find((cond) => isWhereByField(field, cond)),
|
176
|
+
where.filter((cond) => !isWhereByField(field, cond)),
|
177
|
+
];
|
178
|
+
}
|
@@ -140,6 +140,8 @@ export const Image = forwardRef<HTMLImageElement, ImageProps>(function Image(
|
|
140
140
|
return lazyPlaceholder;
|
141
141
|
}
|
142
142
|
|
143
|
+
if (image === undefined)
|
144
|
+
return "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==";
|
143
145
|
if (!image) return undefined;
|
144
146
|
|
145
147
|
const bestImage = highestResAvailable(
|
@@ -13,17 +13,35 @@ describe("Image", async () => {
|
|
13
13
|
vi.spyOn(Account, "getMe").mockReturnValue(account);
|
14
14
|
|
15
15
|
describe("initial rendering", () => {
|
16
|
-
it("should render
|
16
|
+
it("should render a blank placeholder while waiting for the coValue to load", async () => {
|
17
17
|
const { container } = render(
|
18
18
|
<Image imageId="co_zMTubMby3QiKDYnW9e2BEXW7Xaq" alt="test" />,
|
19
19
|
{ account },
|
20
20
|
);
|
21
|
+
|
21
22
|
const img = container.querySelector("img");
|
22
23
|
expect(img).toBeDefined();
|
23
24
|
expect(img!.getAttribute("width")).toBe(null);
|
24
25
|
expect(img!.getAttribute("height")).toBe(null);
|
25
26
|
expect(img!.alt).toBe("test");
|
26
|
-
expect(img!.src).toBe(
|
27
|
+
expect(img!.src).toBe(
|
28
|
+
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==",
|
29
|
+
);
|
30
|
+
});
|
31
|
+
|
32
|
+
it("should render nothing if coValue is not found", async () => {
|
33
|
+
const { container } = render(
|
34
|
+
<Image imageId="co_zMTubMby3QiKDYnW9e2BEXW7Xaq" alt="test" />,
|
35
|
+
{ account },
|
36
|
+
);
|
37
|
+
await waitFor(() => {
|
38
|
+
const img = container.querySelector("img");
|
39
|
+
expect(img).toBeDefined();
|
40
|
+
expect(img!.getAttribute("width")).toBe(null);
|
41
|
+
expect(img!.getAttribute("height")).toBe(null);
|
42
|
+
expect(img!.alt).toBe("test");
|
43
|
+
expect(img!.src).toBe("");
|
44
|
+
});
|
27
45
|
});
|
28
46
|
|
29
47
|
it("should render an empty image if the image is not loaded yet", async () => {
|
@@ -69,7 +69,10 @@ export const Image = forwardRef<RNImage, ImageProps>(function Image(
|
|
69
69
|
ref,
|
70
70
|
) {
|
71
71
|
const image = useCoState(ImageDefinition, imageId);
|
72
|
-
const [src, setSrc] = useState<string | undefined>(
|
72
|
+
const [src, setSrc] = useState<string | undefined>(
|
73
|
+
image?.placeholderDataURL ??
|
74
|
+
"data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==",
|
75
|
+
);
|
73
76
|
|
74
77
|
const dimensions: { width: number | undefined; height: number | undefined } =
|
75
78
|
useMemo(() => {
|
@@ -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());
|