@spooky-sync/client-solid 0.0.0-canary.1 → 0.0.1-alpha.2
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/AGENTS.md +69 -0
- package/README.md +20 -0
- package/dist/index.cjs +815 -59
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +393 -23
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +393 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +805 -61
- package/dist/index.js.map +1 -1
- package/package.json +10 -8
- package/skills/sp00ky-solid/SKILL.md +331 -0
- package/skills/sp00ky-solid/references/file-hooks.md +141 -0
- package/src/cache/index.ts +1 -1
- package/src/cache/surrealdb-wasm-factory.ts +4 -1
- package/src/index.ts +188 -57
- package/src/lib/Blurhash.ts +54 -0
- package/src/lib/BucketImage.ts +141 -0
- package/src/lib/Sp00kyProvider.ts +104 -0
- package/src/lib/context.ts +3 -3
- package/src/lib/create-preload.ts +112 -0
- package/src/lib/models.ts +1 -1
- package/src/lib/use-app-release.ts +89 -0
- package/src/lib/use-blurhash.ts +77 -0
- package/src/lib/use-bucket-image.ts +100 -0
- package/src/lib/use-crdt-field.ts +68 -0
- package/src/lib/use-download-file.ts +175 -0
- package/src/lib/use-feature-flag.ts +50 -0
- package/src/lib/use-file-upload.ts +141 -0
- package/src/lib/use-query.ts +175 -28
- package/src/lib/use-storage-status.ts +44 -0
- package/src/lib/use-sync-status.ts +64 -0
- package/src/types/index.ts +3 -4
- package/src/lib/SpookyProvider.ts +0 -55
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
# File Hooks Reference
|
|
2
|
+
|
|
3
|
+
## useFileUpload
|
|
4
|
+
|
|
5
|
+
Upload, download, and manage files in a SurrealDB bucket.
|
|
6
|
+
|
|
7
|
+
### Signatures
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
// Context-based (inside Sp00kyProvider)
|
|
11
|
+
useFileUpload<S>(bucketName: BucketNames<S>): FileUploadResult;
|
|
12
|
+
|
|
13
|
+
// Explicit db
|
|
14
|
+
useFileUpload<S>(db: SyncedDb<S>, bucketName: BucketNames<S>): FileUploadResult;
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### Return Value
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
interface FileUploadResult {
|
|
21
|
+
isUploading: () => boolean;
|
|
22
|
+
error: () => Error | null;
|
|
23
|
+
clearError: () => void;
|
|
24
|
+
upload: (path: string, file: File | Blob) => Promise<void>;
|
|
25
|
+
download: (path: string) => Promise<string | null>; // Returns object URL
|
|
26
|
+
remove: (path: string) => Promise<void>;
|
|
27
|
+
exists: (path: string) => Promise<boolean>;
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Validation
|
|
32
|
+
|
|
33
|
+
If the bucket has `maxSize` or `allowedExtensions` configured in the schema, the hook validates files before upload and sets `error()` on failure.
|
|
34
|
+
|
|
35
|
+
### Example
|
|
36
|
+
|
|
37
|
+
```tsx
|
|
38
|
+
function AvatarUpload() {
|
|
39
|
+
const { upload, isUploading, error, clearError } = useFileUpload('avatars');
|
|
40
|
+
|
|
41
|
+
const handleFile = async (e: Event) => {
|
|
42
|
+
const file = (e.target as HTMLInputElement).files?.[0];
|
|
43
|
+
if (file) {
|
|
44
|
+
await upload(`user/${userId()}/avatar.png`, file);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div>
|
|
50
|
+
<input type="file" onChange={handleFile} disabled={isUploading()} />
|
|
51
|
+
<Show when={error()}>
|
|
52
|
+
<p class="error">{error()!.message}</p>
|
|
53
|
+
<button onClick={clearError}>Dismiss</button>
|
|
54
|
+
</Show>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## useDownloadFile
|
|
61
|
+
|
|
62
|
+
Reactively download a file from a bucket. Re-fetches when the path changes.
|
|
63
|
+
|
|
64
|
+
### Signatures
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
interface UseDownloadFileOptions {
|
|
68
|
+
cache?: boolean; // default true — false disables every layer
|
|
69
|
+
persist?: boolean; // default true — keep bytes in OPFS
|
|
70
|
+
pin?: boolean; // exempt from pressure eviction
|
|
71
|
+
revalidate?: 'never' | 'head'; // default 'never' (paths are immutable)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Context-based
|
|
75
|
+
useDownloadFile<S>(
|
|
76
|
+
bucketName: BucketNames<S>,
|
|
77
|
+
path: Accessor<string | null | undefined>,
|
|
78
|
+
options?: UseDownloadFileOptions,
|
|
79
|
+
): UseDownloadFileResult;
|
|
80
|
+
|
|
81
|
+
// Explicit db
|
|
82
|
+
useDownloadFile<S>(
|
|
83
|
+
db: SyncedDb<S>,
|
|
84
|
+
bucketName: BucketNames<S>,
|
|
85
|
+
path: Accessor<string | null | undefined>,
|
|
86
|
+
options?: UseDownloadFileOptions,
|
|
87
|
+
): UseDownloadFileResult;
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Return Value
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
interface UseDownloadFileResult {
|
|
94
|
+
url: Accessor<string | null>; // Object URL for the file
|
|
95
|
+
isLoading: Accessor<boolean>;
|
|
96
|
+
error: Accessor<Error | null>;
|
|
97
|
+
refetch: () => void; // Force re-download, bypassing every layer
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Caching
|
|
102
|
+
|
|
103
|
+
Three layers, checked in order:
|
|
104
|
+
|
|
105
|
+
1. **Object URLs**, refcounted per `bucket:path` and shared between components. Revoked once the last holder releases and the entry ages out of a 32-entry hot window.
|
|
106
|
+
2. **OPFS**, under `sp00ky-blobs/<bucketId>/<bucket>/<path>`. Survives reload, works offline, and is namespaced per signed-in user.
|
|
107
|
+
3. **The bucket**, over the sync WebSocket.
|
|
108
|
+
|
|
109
|
+
Nothing expires on a timer — an image whose row is still cached locally has to stay viewable offline. Bytes are dropped only when the app invalidates the path (`bucket.put`/`bucket.delete` do this automatically), when boot reconcile finds no file behind a row, or when the cache exceeds its byte budget, in which case the least-recently-used unpinned entries that nothing is rendering go first.
|
|
110
|
+
|
|
111
|
+
Configure with `blobCache: { enabled, maxBytes, clearOnSignOut }` on the client. The budget defaults to `min(512 MB, quota × 0.25)`. Inspect live numbers in the DevTools Storage tab under "Bucket file cache".
|
|
112
|
+
|
|
113
|
+
`persist: false` keeps the in-tab sharing but writes nothing durable. `cache: false` gives each hook instance a private URL fetched fresh and revoked on unmount.
|
|
114
|
+
|
|
115
|
+
A bucket path is treated as immutable, which is how paths are normally written (`crypto.randomUUID() + ext`). If your app overwrites a path in place from another device, pass `revalidate: 'head'` — it spends a remote `head()` to compare sizes before trusting the cached copy, and keeps the cached copy when that call fails so going offline never blanks an image.
|
|
116
|
+
|
|
117
|
+
### Pinning and prefetching
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
const bucket = db.bucket('avatars');
|
|
121
|
+
await bucket.prefetch(paths); // warm the cache for offline use
|
|
122
|
+
bucket.pin('logo.png'); // never evicted under pressure
|
|
123
|
+
bucket.unpin('logo.png');
|
|
124
|
+
await bucket.evict('old.png'); // drop locally, leave the remote file alone
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Example
|
|
128
|
+
|
|
129
|
+
```tsx
|
|
130
|
+
function Avatar(props: { path: string | null }) {
|
|
131
|
+
const { url, isLoading } = useDownloadFile('avatars', () => props.path);
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<Show when={!isLoading()} fallback={<Spinner />}>
|
|
135
|
+
<Show when={url()}>
|
|
136
|
+
<img src={url()!} alt="Avatar" />
|
|
137
|
+
</Show>
|
|
138
|
+
</Show>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
```
|
package/src/cache/index.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { Diagnostic
|
|
1
|
+
import type { Diagnostic} from 'surrealdb';
|
|
2
|
+
import { Surreal, applyDiagnostics } from 'surrealdb';
|
|
2
3
|
import { createWasmEngines } from '@surrealdb/wasm';
|
|
3
4
|
import type { CacheStrategy } from '../types';
|
|
4
5
|
|
|
5
6
|
const printDiagnostic = ({ key, type, phase, ...other }: Diagnostic) => {
|
|
6
7
|
if (phase === 'progress' || phase === 'after') {
|
|
8
|
+
// oxlint-disable-next-line no-console -- intentional diagnostic logging
|
|
7
9
|
console.log(`[SurrealDB_WASM] [${key}] ${type}:${phase}\n${JSON.stringify(other, null, 2)}`);
|
|
8
10
|
}
|
|
9
11
|
};
|
|
@@ -11,6 +13,7 @@ const printDiagnostic = ({ key, type, phase, ...other }: Diagnostic) => {
|
|
|
11
13
|
/**
|
|
12
14
|
* SurrealDB WASM client factory for different storage strategies
|
|
13
15
|
*/
|
|
16
|
+
// oxlint-disable-next-line no-extraneous-class -- factory pattern groups related static methods
|
|
14
17
|
export class SurrealDBWasmFactory {
|
|
15
18
|
/**
|
|
16
19
|
* Creates a SurrealDB WASM instance with the specified storage strategy
|
package/src/index.ts
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
import type { SyncedDbConfig } from './types';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
type
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
Sp00kyClient,
|
|
4
|
+
type Sp00kyQueryResultPromise,
|
|
5
|
+
type AuthService,
|
|
6
|
+
type BucketHandle,
|
|
7
|
+
type UpdateOptions,
|
|
8
|
+
type RunOptions,
|
|
9
|
+
type SyncHealth,
|
|
10
|
+
type StorageHealth,
|
|
11
|
+
type PreloadOptions,
|
|
12
|
+
type PreloadRefresh,
|
|
8
13
|
} from '@spooky-sync/core';
|
|
9
14
|
|
|
10
|
-
import {
|
|
15
|
+
import type {
|
|
11
16
|
GetTable,
|
|
12
17
|
QueryBuilder,
|
|
13
18
|
SchemaStructure,
|
|
@@ -18,21 +23,63 @@ import {
|
|
|
18
23
|
RelationshipFieldsFromSchema,
|
|
19
24
|
GetRelationship,
|
|
20
25
|
RelatedFieldMapEntry,
|
|
26
|
+
FinalQuery,
|
|
21
27
|
InnerQuery,
|
|
22
28
|
BackendNames,
|
|
23
29
|
BackendRoutes,
|
|
24
30
|
RoutePayload,
|
|
31
|
+
BucketNames,
|
|
32
|
+
BucketDefinitionSchema,
|
|
33
|
+
QueryModifier,
|
|
34
|
+
QueryModifierBuilder,
|
|
35
|
+
QueryInfo,
|
|
36
|
+
RelationshipsMetadata,
|
|
37
|
+
RelationshipDefinition,
|
|
38
|
+
InferRelatedModelFromMetadata,
|
|
39
|
+
GetCardinality,
|
|
25
40
|
} from '@spooky-sync/query-builder';
|
|
26
41
|
|
|
27
|
-
import { RecordId, Uuid, Surreal } from 'surrealdb';
|
|
42
|
+
import { RecordId, Uuid, type Surreal } from 'surrealdb';
|
|
28
43
|
export { RecordId, Uuid };
|
|
29
44
|
export type { Model, GenericModel, GenericSchema, ModelPayload } from './lib/models';
|
|
30
45
|
export { useQuery } from './lib/use-query';
|
|
31
|
-
export {
|
|
46
|
+
export { createPreload } from './lib/create-preload';
|
|
47
|
+
export type { PreloadOptions, PreloadRefresh } from '@spooky-sync/core';
|
|
48
|
+
export { useSyncStatus, type UseSyncStatus } from './lib/use-sync-status';
|
|
49
|
+
export type {
|
|
50
|
+
SyncHealth,
|
|
51
|
+
SyncHealthStatus,
|
|
52
|
+
SyncHealthConfig,
|
|
53
|
+
ConnectionState,
|
|
54
|
+
ReconnectConfig,
|
|
55
|
+
} from '@spooky-sync/core';
|
|
56
|
+
export { useStorageStatus, type UseStorageStatus } from './lib/use-storage-status';
|
|
57
|
+
export type { StorageHealth, StorageHealthStatus } from '@spooky-sync/core';
|
|
58
|
+
export { useCrdtField } from './lib/use-crdt-field';
|
|
59
|
+
export { useFeatureFlag, type UseFeatureFlag } from './lib/use-feature-flag';
|
|
60
|
+
export {
|
|
61
|
+
useAppRelease,
|
|
62
|
+
type UseAppRelease,
|
|
63
|
+
type UseAppReleaseOptions,
|
|
64
|
+
} from './lib/use-app-release';
|
|
65
|
+
export { useFileUpload, type FileUploadResult } from './lib/use-file-upload';
|
|
66
|
+
export {
|
|
67
|
+
useDownloadFile,
|
|
68
|
+
type UseDownloadFileOptions,
|
|
69
|
+
type UseDownloadFileResult,
|
|
70
|
+
} from './lib/use-download-file';
|
|
71
|
+
export { useBlurhash, type UseBlurhashResult } from './lib/use-blurhash';
|
|
72
|
+
export {
|
|
73
|
+
useBucketImage,
|
|
74
|
+
type UseBucketImageOptions,
|
|
75
|
+
type UseBucketImageResult,
|
|
76
|
+
} from './lib/use-bucket-image';
|
|
77
|
+
export { Blurhash, type BlurhashProps } from './lib/Blurhash';
|
|
78
|
+
export { BucketImage, type BucketImageProps } from './lib/BucketImage';
|
|
79
|
+
export { Sp00kyProvider, type Sp00kyProviderProps } from './lib/Sp00kyProvider';
|
|
32
80
|
export { useDb } from './lib/context';
|
|
33
81
|
|
|
34
82
|
// export { AuthEventTypes } from "@spooky-sync/core"; // TODO: Verify if AuthEventTypes exists in core
|
|
35
|
-
export type {};
|
|
36
83
|
|
|
37
84
|
// Re-export query builder types for convenience
|
|
38
85
|
export type {
|
|
@@ -47,7 +94,7 @@ export type {
|
|
|
47
94
|
TableModel,
|
|
48
95
|
TableNames,
|
|
49
96
|
QueryResult,
|
|
50
|
-
}
|
|
97
|
+
};
|
|
51
98
|
|
|
52
99
|
export type RelationshipField<
|
|
53
100
|
Schema extends SchemaStructure,
|
|
@@ -89,39 +136,52 @@ export type WithRelatedMany<Field extends string, RelatedFields extends RelatedF
|
|
|
89
136
|
};
|
|
90
137
|
|
|
91
138
|
/**
|
|
92
|
-
* SyncedDb - A thin wrapper around
|
|
93
|
-
* Delegates all logic to the underlying
|
|
139
|
+
* SyncedDb - A thin wrapper around sp00ky-ts for Solid.js integration
|
|
140
|
+
* Delegates all logic to the underlying sp00ky-ts instance
|
|
94
141
|
*/
|
|
95
142
|
export class SyncedDb<S extends SchemaStructure> {
|
|
96
143
|
private config: SyncedDbConfig<S>;
|
|
97
|
-
private
|
|
144
|
+
private sp00ky: Sp00kyClient<S> | null = null;
|
|
98
145
|
private _initialized = false;
|
|
99
146
|
|
|
100
147
|
constructor(config: SyncedDbConfig<S>) {
|
|
101
148
|
this.config = config;
|
|
102
149
|
}
|
|
103
150
|
|
|
104
|
-
public
|
|
105
|
-
if (!this.
|
|
106
|
-
return this.
|
|
151
|
+
public getSp00ky(): Sp00kyClient<S> {
|
|
152
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
153
|
+
return this.sp00ky;
|
|
107
154
|
}
|
|
108
155
|
|
|
109
156
|
/**
|
|
110
|
-
* Initialize the
|
|
157
|
+
* Initialize the sp00ky-ts instance
|
|
111
158
|
*/
|
|
112
159
|
async init(): Promise<void> {
|
|
113
160
|
if (this._initialized) return;
|
|
114
|
-
this.
|
|
115
|
-
await this.
|
|
161
|
+
this.sp00ky = new Sp00kyClient<S>(this.config);
|
|
162
|
+
await this.sp00ky.init();
|
|
116
163
|
this._initialized = true;
|
|
117
164
|
}
|
|
118
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Tear down the client: leaves the tabs broker, closes the local store and
|
|
168
|
+
* remote socket, and frees the wasm circuit. Without this a remounted provider
|
|
169
|
+
* (or an HMR reload) strands a whole client, and the abandoned wasm heaps stay
|
|
170
|
+
* resident because V8 cannot see how much wasm memory a dropped wrapper holds.
|
|
171
|
+
*/
|
|
172
|
+
async close(): Promise<void> {
|
|
173
|
+
const instance = this.sp00ky;
|
|
174
|
+
this.sp00ky = null;
|
|
175
|
+
this._initialized = false;
|
|
176
|
+
if (instance) await instance.close();
|
|
177
|
+
}
|
|
178
|
+
|
|
119
179
|
/**
|
|
120
180
|
* Create a new record in the database
|
|
121
181
|
*/
|
|
122
182
|
async create(id: string, payload: Record<string, unknown>): Promise<void> {
|
|
123
|
-
if (!this.
|
|
124
|
-
await this.
|
|
183
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
184
|
+
await this.sp00ky.create(id, payload as Record<string, unknown>);
|
|
125
185
|
}
|
|
126
186
|
|
|
127
187
|
/**
|
|
@@ -133,8 +193,8 @@ export class SyncedDb<S extends SchemaStructure> {
|
|
|
133
193
|
payload: Partial<TableModel<GetTable<S, TName>>>,
|
|
134
194
|
options?: UpdateOptions
|
|
135
195
|
): Promise<void> {
|
|
136
|
-
if (!this.
|
|
137
|
-
await this.
|
|
196
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
197
|
+
await this.sp00ky.update(
|
|
138
198
|
tableName as string,
|
|
139
199
|
recordId,
|
|
140
200
|
payload as Record<string, unknown>,
|
|
@@ -147,12 +207,39 @@ export class SyncedDb<S extends SchemaStructure> {
|
|
|
147
207
|
*/
|
|
148
208
|
async delete<TName extends TableNames<S>>(
|
|
149
209
|
tableName: TName,
|
|
150
|
-
selector: string | InnerQuery<GetTable<S, TName>, boolean>
|
|
210
|
+
selector: string | RecordId | InnerQuery<GetTable<S, TName>, boolean>
|
|
151
211
|
): Promise<void> {
|
|
152
|
-
if (!this.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
212
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
213
|
+
// Accept a `"table:id"` string OR a RecordId — live-query rows carry their
|
|
214
|
+
// `id` as a RecordId, so callers can pass `db.delete('game', row.id)`
|
|
215
|
+
// directly. Build the canonical string from the raw id part (not
|
|
216
|
+
// `RecordId.toString()`, which escapes special chars) so it round-trips
|
|
217
|
+
// through the engine's `parseRecordIdString`. InnerQuery selectors are not
|
|
218
|
+
// supported yet. (cross-package RecordId instances → match by constructor name.)
|
|
219
|
+
const isRecordId =
|
|
220
|
+
selector instanceof RecordId || (selector as any)?.constructor?.name === 'RecordId';
|
|
221
|
+
let id: string;
|
|
222
|
+
if (typeof selector === 'string') {
|
|
223
|
+
id = selector;
|
|
224
|
+
} else if (isRecordId) {
|
|
225
|
+
id = `${tableName as string}:${(selector as RecordId).id}`;
|
|
226
|
+
} else {
|
|
227
|
+
throw new Error('Only string ID or RecordId selectors are supported currently with core');
|
|
228
|
+
}
|
|
229
|
+
await this.sp00ky.delete(tableName as string, id);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Preload/prewarm a built query into the local cache without registering a
|
|
234
|
+
* live view. Fetches once and stores the rows (+ embedded related children)
|
|
235
|
+
* locally so a later `useQuery` for the same data paints instantly. Best-effort.
|
|
236
|
+
*/
|
|
237
|
+
public async preload(
|
|
238
|
+
finalQuery: FinalQuery<S, any, any, any, any, Sp00kyQueryResultPromise>,
|
|
239
|
+
options?: PreloadOptions
|
|
240
|
+
): Promise<void> {
|
|
241
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
242
|
+
await this.sp00ky.preload(finalQuery, options);
|
|
156
243
|
}
|
|
157
244
|
|
|
158
245
|
/**
|
|
@@ -160,34 +247,31 @@ export class SyncedDb<S extends SchemaStructure> {
|
|
|
160
247
|
*/
|
|
161
248
|
public query<TName extends TableNames<S>>(
|
|
162
249
|
table: TName
|
|
163
|
-
): QueryBuilder<S, TName,
|
|
164
|
-
if (!this.
|
|
165
|
-
return this.
|
|
250
|
+
): QueryBuilder<S, TName, Sp00kyQueryResultPromise, {}, false> {
|
|
251
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
252
|
+
return this.sp00ky.query(table, {});
|
|
166
253
|
}
|
|
167
254
|
|
|
168
255
|
/**
|
|
169
256
|
* Run a backend operation
|
|
170
257
|
*/
|
|
171
|
-
public async run<
|
|
172
|
-
B extends BackendNames<S>,
|
|
173
|
-
R extends BackendRoutes<S, B>,
|
|
174
|
-
>(
|
|
258
|
+
public async run<B extends BackendNames<S>, R extends BackendRoutes<S, B>>(
|
|
175
259
|
backend: B,
|
|
176
260
|
path: R,
|
|
177
261
|
payload: RoutePayload<S, B, R>,
|
|
178
|
-
options?: RunOptions
|
|
262
|
+
options?: RunOptions
|
|
179
263
|
): Promise<void> {
|
|
180
|
-
if (!this.
|
|
181
|
-
await this.
|
|
264
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
265
|
+
await this.sp00ky.run(backend, path, payload, options);
|
|
182
266
|
}
|
|
183
267
|
|
|
184
268
|
/**
|
|
185
269
|
* Authenticate with the database
|
|
186
270
|
*/
|
|
187
271
|
public async authenticate(token: string): Promise<RecordId<string>> {
|
|
188
|
-
|
|
189
|
-
//
|
|
190
|
-
// Wait, checked
|
|
272
|
+
await this.sp00ky?.authenticate(token);
|
|
273
|
+
// Sp00kyClient.authenticate returns whatever remote.authenticate returns (boolean or token usually?)
|
|
274
|
+
// Wait, checked Sp00kyClient: return this.remote.getClient().authenticate(token);
|
|
191
275
|
// SurrealDB authenticate returns void? or token?
|
|
192
276
|
// Assuming void or token.
|
|
193
277
|
return new RecordId('user', 'me'); // Placeholder or actual?
|
|
@@ -205,49 +289,96 @@ export class SyncedDb<S extends SchemaStructure> {
|
|
|
205
289
|
* Sign out, clear session and local storage
|
|
206
290
|
*/
|
|
207
291
|
public async signOut(): Promise<void> {
|
|
208
|
-
if (!this.
|
|
209
|
-
await this.
|
|
292
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
293
|
+
await this.sp00ky.auth.signOut();
|
|
210
294
|
}
|
|
211
295
|
|
|
212
296
|
/**
|
|
213
297
|
* Execute a function with direct access to the remote database connection
|
|
214
298
|
*/
|
|
215
299
|
public async useRemote<T>(fn: (db: Surreal) => T | Promise<T>): Promise<T> {
|
|
216
|
-
if (!this.
|
|
217
|
-
return await this.
|
|
300
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
301
|
+
return await this.sp00ky.useRemote(fn);
|
|
218
302
|
}
|
|
219
303
|
/**
|
|
220
304
|
* Access the remote database service directly
|
|
221
305
|
*/
|
|
222
|
-
get remote():
|
|
223
|
-
if (!this.
|
|
224
|
-
return this.
|
|
306
|
+
get remote(): Sp00kyClient<S>['remoteClient'] {
|
|
307
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
308
|
+
return this.sp00ky.remoteClient;
|
|
225
309
|
}
|
|
226
310
|
|
|
227
311
|
/**
|
|
228
312
|
* Access the local database service directly
|
|
229
313
|
*/
|
|
230
|
-
get local():
|
|
231
|
-
if (!this.
|
|
232
|
-
return this.
|
|
314
|
+
get local(): Sp00kyClient<S>['localClient'] {
|
|
315
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
316
|
+
return this.sp00ky.localClient;
|
|
233
317
|
}
|
|
234
318
|
|
|
235
319
|
/**
|
|
236
320
|
* Access the auth service
|
|
237
321
|
*/
|
|
238
322
|
get auth(): AuthService<S> {
|
|
239
|
-
if (!this.
|
|
240
|
-
return this.
|
|
323
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
324
|
+
return this.sp00ky.auth;
|
|
241
325
|
}
|
|
242
326
|
|
|
243
327
|
get pendingMutationCount(): number {
|
|
244
|
-
if (!this.
|
|
245
|
-
return this.
|
|
328
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
329
|
+
return this.sp00ky.pendingMutationCount;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Diagnostic — see `Sp00kyClient.liveRetryCount`. */
|
|
333
|
+
get liveRetryCount(): number {
|
|
334
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
335
|
+
return this.sp00ky.liveRetryCount;
|
|
246
336
|
}
|
|
247
337
|
|
|
248
338
|
subscribeToPendingMutations(cb: (count: number) => void): () => void {
|
|
249
|
-
if (!this.
|
|
250
|
-
return this.
|
|
339
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
340
|
+
return this.sp00ky.subscribeToPendingMutations(cb);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** Current sync-health snapshot. See {@link useSyncStatus}. */
|
|
344
|
+
get syncHealth(): SyncHealth {
|
|
345
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
346
|
+
return this.sp00ky.syncHealth;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Observe sync health. Fires immediately with the current status and again
|
|
351
|
+
* on every healthy↔degraded transition. Prefer the `useSyncStatus` hook in
|
|
352
|
+
* components; this is the imperative escape hatch.
|
|
353
|
+
*/
|
|
354
|
+
subscribeToSyncHealth(cb: (health: SyncHealth) => void): () => void {
|
|
355
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
356
|
+
return this.sp00ky.subscribeToSyncHealth(cb);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Current local-store durability snapshot. See {@link useStorageStatus}. */
|
|
360
|
+
get storageHealth(): StorageHealth {
|
|
361
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
362
|
+
return this.sp00ky.storageHealth;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Observe local-store durability. Fires immediately with the current snapshot
|
|
367
|
+
* and again on change. Prefer the `useStorageStatus` hook in components; this
|
|
368
|
+
* is the imperative escape hatch.
|
|
369
|
+
*/
|
|
370
|
+
subscribeToStorageHealth(cb: (health: StorageHealth) => void): () => void {
|
|
371
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
372
|
+
return this.sp00ky.subscribeToStorageHealth(cb);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
bucket<B extends BucketNames<S>>(name: B): BucketHandle {
|
|
376
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
377
|
+
return this.sp00ky.bucket(name);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
getBucketConfig(name: string): BucketDefinitionSchema | undefined {
|
|
381
|
+
return this.config.schema.buckets?.find((b) => b.name === name);
|
|
251
382
|
}
|
|
252
383
|
}
|
|
253
384
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createEffect } from 'solid-js';
|
|
2
|
+
import type { JSX } from 'solid-js';
|
|
3
|
+
import { decodeBlurhash } from '@spooky-sync/core';
|
|
4
|
+
|
|
5
|
+
export interface BlurhashProps {
|
|
6
|
+
/** The blurhash string. Nullish paints nothing (transparent canvas). */
|
|
7
|
+
hash: string | null | undefined;
|
|
8
|
+
/** Decode resolution. 32x32 is plenty: blurhash carries at most 9x9 DCT
|
|
9
|
+
* components, the canvas is meant to be CSS-scaled to fill. */
|
|
10
|
+
width?: number;
|
|
11
|
+
height?: number;
|
|
12
|
+
/** Contrast punch, see the blurhash reference decoder. Defaults to 1. */
|
|
13
|
+
punch?: number;
|
|
14
|
+
class?: string;
|
|
15
|
+
style?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A blurhash painted onto a canvas, once per hash change. Size the canvas via
|
|
20
|
+
* `class`/`style` (e.g. `absolute inset-0 w-full h-full`); the internal decode
|
|
21
|
+
* resolution stays tiny regardless of the displayed size.
|
|
22
|
+
*/
|
|
23
|
+
export function Blurhash(props: BlurhashProps): JSX.Element {
|
|
24
|
+
if (typeof document === 'undefined') return null;
|
|
25
|
+
const canvas = document.createElement('canvas');
|
|
26
|
+
|
|
27
|
+
createEffect(() => {
|
|
28
|
+
canvas.className = props.class ?? '';
|
|
29
|
+
});
|
|
30
|
+
createEffect(() => {
|
|
31
|
+
canvas.style.cssText = props.style ?? '';
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
createEffect(() => {
|
|
35
|
+
const width = props.width ?? 32;
|
|
36
|
+
const height = props.height ?? 32;
|
|
37
|
+
canvas.width = width;
|
|
38
|
+
canvas.height = height;
|
|
39
|
+
const hash = props.hash;
|
|
40
|
+
if (!hash) return;
|
|
41
|
+
try {
|
|
42
|
+
const pixels = decodeBlurhash(hash, width, height, props.punch ?? 1);
|
|
43
|
+
const ctx = canvas.getContext('2d');
|
|
44
|
+
if (!ctx) return;
|
|
45
|
+
const imageData = ctx.createImageData(width, height);
|
|
46
|
+
imageData.data.set(pixels);
|
|
47
|
+
ctx.putImageData(imageData, 0, 0);
|
|
48
|
+
} catch {
|
|
49
|
+
// An invalid hash paints nothing; the layer below stays visible.
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return canvas;
|
|
54
|
+
}
|