@spooky-sync/client-solid 0.0.1-canary.197 → 0.0.1-canary.199
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/dist/index.cjs +214 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +116 -3
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +116 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +213 -5
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/index.ts +8 -0
- package/src/lib/Blurhash.ts +54 -0
- package/src/lib/BucketImage.ts +137 -0
- package/src/lib/use-blurhash.ts +77 -0
- package/src/lib/use-bucket-image.ts +100 -0
- package/src/lib/use-file-upload.ts +8 -3
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createSignal, createEffect, onCleanup, type Accessor } from 'solid-js';
|
|
2
|
+
import type { SchemaStructure, BucketNames } from '@spooky-sync/query-builder';
|
|
3
|
+
import type { SyncedDb } from '../index';
|
|
4
|
+
import { useDb } from './context';
|
|
5
|
+
|
|
6
|
+
export interface UseBlurhashResult {
|
|
7
|
+
/** The stored blurhash for the path, or null while loading / when none exists. */
|
|
8
|
+
hash: Accessor<string | null>;
|
|
9
|
+
isLoading: Accessor<boolean>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The blurhash sidecar for a bucket image (written automatically by
|
|
14
|
+
* `bucket.put`, see `Sp00kyConfig.blurhash`). Resolves from OPFS instantly on
|
|
15
|
+
* warm clients; a miss is remembered per tab. Use this directly when the hash
|
|
16
|
+
* belongs to a different rendition than the displayed image; otherwise
|
|
17
|
+
* `useBucketImage` / `BucketImage` bundle it with the download.
|
|
18
|
+
*/
|
|
19
|
+
export function useBlurhash<S extends SchemaStructure>(
|
|
20
|
+
bucketName: BucketNames<S>,
|
|
21
|
+
path: Accessor<string | null | undefined>
|
|
22
|
+
): UseBlurhashResult;
|
|
23
|
+
export function useBlurhash<S extends SchemaStructure>(
|
|
24
|
+
db: SyncedDb<S>,
|
|
25
|
+
bucketName: BucketNames<S>,
|
|
26
|
+
path: Accessor<string | null | undefined>
|
|
27
|
+
): UseBlurhashResult;
|
|
28
|
+
export function useBlurhash<S extends SchemaStructure>(
|
|
29
|
+
dbOrBucketName: SyncedDb<S> | BucketNames<S>,
|
|
30
|
+
bucketNameOrPath?: BucketNames<S> | Accessor<string | null | undefined>,
|
|
31
|
+
maybePath?: Accessor<string | null | undefined>
|
|
32
|
+
): UseBlurhashResult {
|
|
33
|
+
let db: SyncedDb<S>;
|
|
34
|
+
let bucketName: BucketNames<S>;
|
|
35
|
+
let path: Accessor<string | null | undefined>;
|
|
36
|
+
|
|
37
|
+
if (typeof dbOrBucketName === 'string') {
|
|
38
|
+
db = useDb<S>();
|
|
39
|
+
bucketName = dbOrBucketName as BucketNames<S>;
|
|
40
|
+
path = bucketNameOrPath as Accessor<string | null | undefined>;
|
|
41
|
+
} else {
|
|
42
|
+
db = dbOrBucketName as SyncedDb<S>;
|
|
43
|
+
bucketName = bucketNameOrPath as BucketNames<S>;
|
|
44
|
+
path = maybePath as Accessor<string | null | undefined>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const [hash, setHash] = createSignal<string | null>(null);
|
|
48
|
+
const [isLoading, setIsLoading] = createSignal(false);
|
|
49
|
+
|
|
50
|
+
createEffect(() => {
|
|
51
|
+
const filePath = path();
|
|
52
|
+
if (!filePath) {
|
|
53
|
+
setHash(null);
|
|
54
|
+
setIsLoading(false);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
let cancelled = false;
|
|
58
|
+
setIsLoading(true);
|
|
59
|
+
db.bucket(bucketName)
|
|
60
|
+
.blurhash(filePath)
|
|
61
|
+
.then((result) => {
|
|
62
|
+
if (cancelled) return;
|
|
63
|
+
setHash(result);
|
|
64
|
+
setIsLoading(false);
|
|
65
|
+
})
|
|
66
|
+
.catch(() => {
|
|
67
|
+
if (cancelled) return;
|
|
68
|
+
setHash(null);
|
|
69
|
+
setIsLoading(false);
|
|
70
|
+
});
|
|
71
|
+
onCleanup(() => {
|
|
72
|
+
cancelled = true;
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return { hash, isLoading };
|
|
77
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { createSignal, createEffect, on, type Accessor } from 'solid-js';
|
|
2
|
+
import type { SchemaStructure, BucketNames } from '@spooky-sync/query-builder';
|
|
3
|
+
import type { SyncedDb } from '../index';
|
|
4
|
+
import { useDb } from './context';
|
|
5
|
+
import {
|
|
6
|
+
useDownloadFile,
|
|
7
|
+
type UseDownloadFileOptions,
|
|
8
|
+
type UseDownloadFileResult,
|
|
9
|
+
} from './use-download-file';
|
|
10
|
+
import { useBlurhash } from './use-blurhash';
|
|
11
|
+
|
|
12
|
+
export interface UseBucketImageOptions extends UseDownloadFileOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Also resolve the image's blurhash sidecar (see `Sp00kyConfig.blurhash`).
|
|
15
|
+
* Default `true`; the read is registered before the image bytes so the tiny
|
|
16
|
+
* sidecar tends to land first on the serialized remote chain.
|
|
17
|
+
*/
|
|
18
|
+
blurhash?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface UseBucketImageResult extends UseDownloadFileResult {
|
|
22
|
+
/** Blurhash for the same path, or null (off, missing, still loading). */
|
|
23
|
+
blurhash: Accessor<string | null>;
|
|
24
|
+
/** True once the current `url()` has been decoded and is safe to paint. */
|
|
25
|
+
ready: Accessor<boolean>;
|
|
26
|
+
/**
|
|
27
|
+
* Ref callback for the `<img>` rendering `url()`: flips `ready` when the
|
|
28
|
+
* bitmap is decoded (resolves on failure too, so a broken blob degrades to
|
|
29
|
+
* paint-on-load instead of hiding the image forever). Re-arms itself when
|
|
30
|
+
* the url changes.
|
|
31
|
+
*/
|
|
32
|
+
gate: (img: HTMLImageElement) => void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Everything needed to render a bucket image without a pop-in: the refcounted
|
|
37
|
+
* object URL, the blurhash placeholder, and a decode gate so the real bitmap
|
|
38
|
+
* is only revealed once it can paint in full. `BucketImage` wraps this into a
|
|
39
|
+
* drop-in component; use the hook for custom markup.
|
|
40
|
+
*/
|
|
41
|
+
export function useBucketImage<S extends SchemaStructure>(
|
|
42
|
+
bucketName: BucketNames<S>,
|
|
43
|
+
path: Accessor<string | null | undefined>,
|
|
44
|
+
options?: UseBucketImageOptions
|
|
45
|
+
): UseBucketImageResult;
|
|
46
|
+
export function useBucketImage<S extends SchemaStructure>(
|
|
47
|
+
db: SyncedDb<S>,
|
|
48
|
+
bucketName: BucketNames<S>,
|
|
49
|
+
path: Accessor<string | null | undefined>,
|
|
50
|
+
options?: UseBucketImageOptions
|
|
51
|
+
): UseBucketImageResult;
|
|
52
|
+
export function useBucketImage<S extends SchemaStructure>(
|
|
53
|
+
dbOrBucketName: SyncedDb<S> | BucketNames<S>,
|
|
54
|
+
bucketNameOrPath?: BucketNames<S> | Accessor<string | null | undefined>,
|
|
55
|
+
pathOrOptions?: Accessor<string | null | undefined> | UseBucketImageOptions,
|
|
56
|
+
maybeOptions?: UseBucketImageOptions
|
|
57
|
+
): UseBucketImageResult {
|
|
58
|
+
let db: SyncedDb<S>;
|
|
59
|
+
let bucketName: BucketNames<S>;
|
|
60
|
+
let path: Accessor<string | null | undefined>;
|
|
61
|
+
let options: UseBucketImageOptions;
|
|
62
|
+
|
|
63
|
+
if (typeof dbOrBucketName === 'string') {
|
|
64
|
+
db = useDb<S>();
|
|
65
|
+
bucketName = dbOrBucketName as BucketNames<S>;
|
|
66
|
+
path = bucketNameOrPath as Accessor<string | null | undefined>;
|
|
67
|
+
options = (pathOrOptions as UseBucketImageOptions) ?? {};
|
|
68
|
+
} else {
|
|
69
|
+
db = dbOrBucketName as SyncedDb<S>;
|
|
70
|
+
bucketName = bucketNameOrPath as BucketNames<S>;
|
|
71
|
+
path = pathOrOptions as Accessor<string | null | undefined>;
|
|
72
|
+
options = maybeOptions ?? {};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Registered BEFORE the download so the sidecar read enters the serialized
|
|
76
|
+
// remote queue first: the placeholder should never wait behind the bytes it
|
|
77
|
+
// is standing in for.
|
|
78
|
+
const wantHash = options.blurhash !== false;
|
|
79
|
+
const { hash } = useBlurhash(db, bucketName, () => (wantHash ? path() : null));
|
|
80
|
+
|
|
81
|
+
const file = useDownloadFile(db, bucketName, path, options);
|
|
82
|
+
|
|
83
|
+
const [ready, setReady] = createSignal(false);
|
|
84
|
+
// A new url (path change, refetch) means a new undecoded bitmap.
|
|
85
|
+
createEffect(on(file.url, () => setReady(false), { defer: true }));
|
|
86
|
+
|
|
87
|
+
const gate = (img: HTMLImageElement) => {
|
|
88
|
+
const done = () => setReady(true);
|
|
89
|
+
if (typeof img.decode === 'function') {
|
|
90
|
+
img.decode().then(done, done);
|
|
91
|
+
} else if (img.complete) {
|
|
92
|
+
done();
|
|
93
|
+
} else {
|
|
94
|
+
img.onload = done;
|
|
95
|
+
img.onerror = done;
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return { ...file, blurhash: hash, ready, gate };
|
|
100
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createSignal, onCleanup } from 'solid-js';
|
|
2
2
|
import type { SchemaStructure, BucketNames } from '@spooky-sync/query-builder';
|
|
3
3
|
import { fileToUint8Array } from '@spooky-sync/core';
|
|
4
|
+
import type { BucketPutOptions, BucketPutResult } from '@spooky-sync/core';
|
|
4
5
|
import type { SyncedDb } from '../index';
|
|
5
6
|
import { useDb } from './context';
|
|
6
7
|
|
|
@@ -8,7 +9,7 @@ export interface FileUploadResult {
|
|
|
8
9
|
isUploading: () => boolean;
|
|
9
10
|
error: () => Error | null;
|
|
10
11
|
clearError: () => void;
|
|
11
|
-
upload: (path: string, file: File | Blob) => Promise<void>;
|
|
12
|
+
upload: (path: string, file: File | Blob, options?: BucketPutOptions) => Promise<BucketPutResult | void>;
|
|
12
13
|
download: (path: string) => Promise<string | null>;
|
|
13
14
|
remove: (path: string) => Promise<void>;
|
|
14
15
|
exists: (path: string) => Promise<boolean>;
|
|
@@ -71,7 +72,11 @@ export function useFileUpload<S extends SchemaStructure>(
|
|
|
71
72
|
}
|
|
72
73
|
};
|
|
73
74
|
|
|
74
|
-
const upload = async (
|
|
75
|
+
const upload = async (
|
|
76
|
+
path: string,
|
|
77
|
+
file: File | Blob,
|
|
78
|
+
options?: BucketPutOptions
|
|
79
|
+
): Promise<BucketPutResult | void> => {
|
|
75
80
|
setError(null);
|
|
76
81
|
try {
|
|
77
82
|
validate(file);
|
|
@@ -83,7 +88,7 @@ export function useFileUpload<S extends SchemaStructure>(
|
|
|
83
88
|
setIsUploading(true);
|
|
84
89
|
try {
|
|
85
90
|
const bytes = await fileToUint8Array(file);
|
|
86
|
-
await db.bucket(bucketName).put(path, bytes);
|
|
91
|
+
return await db.bucket(bucketName).put(path, bytes, options);
|
|
87
92
|
} catch (e) {
|
|
88
93
|
setError(e instanceof Error ? e : new Error(String(e)));
|
|
89
94
|
} finally {
|