@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.
@@ -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 (path: string, file: File | Blob): Promise<void> => {
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 {