@spooky-sync/client-solid 0.0.1-canary.9 → 0.0.1-canary.90
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 +66 -0
- package/dist/index.cjs +216 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +88 -21
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +88 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +214 -66
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/skills/sp00ky-solid/SKILL.md +264 -0
- package/skills/sp00ky-solid/references/file-hooks.md +112 -0
- package/src/cache/index.ts +1 -1
- package/src/cache/surrealdb-wasm-factory.ts +4 -1
- package/src/index.ts +103 -55
- package/src/lib/{SpookyProvider.ts → Sp00kyProvider.ts} +9 -7
- package/src/lib/context.ts +3 -3
- package/src/lib/models.ts +1 -1
- package/src/lib/use-crdt-field.ts +68 -0
- package/src/lib/use-download-file.ts +2 -2
- package/src/lib/use-feature-flag.ts +50 -0
- package/src/lib/use-file-upload.ts +2 -1
- package/src/lib/use-query.ts +130 -28
- package/src/lib/use-sync-status.ts +39 -0
- package/src/types/index.ts +3 -4
package/src/index.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import type { SyncedDbConfig } from './types';
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
type
|
|
7
|
-
UpdateOptions,
|
|
8
|
-
RunOptions,
|
|
3
|
+
Sp00kyClient,
|
|
4
|
+
type Sp00kyQueryResultPromise,
|
|
5
|
+
type AuthService,
|
|
6
|
+
type BucketHandle,
|
|
7
|
+
type UpdateOptions,
|
|
8
|
+
type RunOptions,
|
|
9
|
+
type SyncHealth,
|
|
9
10
|
} from '@spooky-sync/core';
|
|
10
11
|
|
|
11
|
-
import {
|
|
12
|
+
import type {
|
|
12
13
|
GetTable,
|
|
13
14
|
QueryBuilder,
|
|
14
15
|
SchemaStructure,
|
|
@@ -25,19 +26,30 @@ import {
|
|
|
25
26
|
RoutePayload,
|
|
26
27
|
BucketNames,
|
|
27
28
|
BucketDefinitionSchema,
|
|
29
|
+
QueryModifier,
|
|
30
|
+
QueryModifierBuilder,
|
|
31
|
+
QueryInfo,
|
|
32
|
+
RelationshipsMetadata,
|
|
33
|
+
RelationshipDefinition,
|
|
34
|
+
InferRelatedModelFromMetadata,
|
|
35
|
+
GetCardinality,
|
|
28
36
|
} from '@spooky-sync/query-builder';
|
|
29
37
|
|
|
30
|
-
import { RecordId, Uuid, Surreal } from 'surrealdb';
|
|
38
|
+
import { RecordId, Uuid, type Surreal } from 'surrealdb';
|
|
31
39
|
export { RecordId, Uuid };
|
|
32
40
|
export type { Model, GenericModel, GenericSchema, ModelPayload } from './lib/models';
|
|
33
41
|
export { useQuery } from './lib/use-query';
|
|
42
|
+
export { useSyncStatus, type UseSyncStatus } from './lib/use-sync-status';
|
|
43
|
+
export type { SyncHealth, SyncHealthStatus, SyncHealthConfig } from '@spooky-sync/core';
|
|
44
|
+
export { useCrdtField } from './lib/use-crdt-field';
|
|
45
|
+
export { useFeatureFlag, type UseFeatureFlag } from './lib/use-feature-flag';
|
|
34
46
|
export { useFileUpload, type FileUploadResult } from './lib/use-file-upload';
|
|
35
47
|
export { useDownloadFile, type UseDownloadFileOptions, type UseDownloadFileResult } from './lib/use-download-file';
|
|
36
|
-
export {
|
|
48
|
+
export { Sp00kyProvider, type Sp00kyProviderProps } from './lib/Sp00kyProvider';
|
|
37
49
|
export { useDb } from './lib/context';
|
|
38
50
|
|
|
39
51
|
// export { AuthEventTypes } from "@spooky-sync/core"; // TODO: Verify if AuthEventTypes exists in core
|
|
40
|
-
|
|
52
|
+
|
|
41
53
|
|
|
42
54
|
// Re-export query builder types for convenience
|
|
43
55
|
export type {
|
|
@@ -52,7 +64,7 @@ export type {
|
|
|
52
64
|
TableModel,
|
|
53
65
|
TableNames,
|
|
54
66
|
QueryResult,
|
|
55
|
-
}
|
|
67
|
+
};
|
|
56
68
|
|
|
57
69
|
export type RelationshipField<
|
|
58
70
|
Schema extends SchemaStructure,
|
|
@@ -94,30 +106,30 @@ export type WithRelatedMany<Field extends string, RelatedFields extends RelatedF
|
|
|
94
106
|
};
|
|
95
107
|
|
|
96
108
|
/**
|
|
97
|
-
* SyncedDb - A thin wrapper around
|
|
98
|
-
* Delegates all logic to the underlying
|
|
109
|
+
* SyncedDb - A thin wrapper around sp00ky-ts for Solid.js integration
|
|
110
|
+
* Delegates all logic to the underlying sp00ky-ts instance
|
|
99
111
|
*/
|
|
100
112
|
export class SyncedDb<S extends SchemaStructure> {
|
|
101
113
|
private config: SyncedDbConfig<S>;
|
|
102
|
-
private
|
|
114
|
+
private sp00ky: Sp00kyClient<S> | null = null;
|
|
103
115
|
private _initialized = false;
|
|
104
116
|
|
|
105
117
|
constructor(config: SyncedDbConfig<S>) {
|
|
106
118
|
this.config = config;
|
|
107
119
|
}
|
|
108
120
|
|
|
109
|
-
public
|
|
110
|
-
if (!this.
|
|
111
|
-
return this.
|
|
121
|
+
public getSp00ky(): Sp00kyClient<S> {
|
|
122
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
123
|
+
return this.sp00ky;
|
|
112
124
|
}
|
|
113
125
|
|
|
114
126
|
/**
|
|
115
|
-
* Initialize the
|
|
127
|
+
* Initialize the sp00ky-ts instance
|
|
116
128
|
*/
|
|
117
129
|
async init(): Promise<void> {
|
|
118
130
|
if (this._initialized) return;
|
|
119
|
-
this.
|
|
120
|
-
await this.
|
|
131
|
+
this.sp00ky = new Sp00kyClient<S>(this.config);
|
|
132
|
+
await this.sp00ky.init();
|
|
121
133
|
this._initialized = true;
|
|
122
134
|
}
|
|
123
135
|
|
|
@@ -125,8 +137,8 @@ export class SyncedDb<S extends SchemaStructure> {
|
|
|
125
137
|
* Create a new record in the database
|
|
126
138
|
*/
|
|
127
139
|
async create(id: string, payload: Record<string, unknown>): Promise<void> {
|
|
128
|
-
if (!this.
|
|
129
|
-
await this.
|
|
140
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
141
|
+
await this.sp00ky.create(id, payload as Record<string, unknown>);
|
|
130
142
|
}
|
|
131
143
|
|
|
132
144
|
/**
|
|
@@ -138,8 +150,8 @@ export class SyncedDb<S extends SchemaStructure> {
|
|
|
138
150
|
payload: Partial<TableModel<GetTable<S, TName>>>,
|
|
139
151
|
options?: UpdateOptions
|
|
140
152
|
): Promise<void> {
|
|
141
|
-
if (!this.
|
|
142
|
-
await this.
|
|
153
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
154
|
+
await this.sp00ky.update(
|
|
143
155
|
tableName as string,
|
|
144
156
|
recordId,
|
|
145
157
|
payload as Record<string, unknown>,
|
|
@@ -152,12 +164,26 @@ export class SyncedDb<S extends SchemaStructure> {
|
|
|
152
164
|
*/
|
|
153
165
|
async delete<TName extends TableNames<S>>(
|
|
154
166
|
tableName: TName,
|
|
155
|
-
selector: string | InnerQuery<GetTable<S, TName>, boolean>
|
|
167
|
+
selector: string | RecordId | InnerQuery<GetTable<S, TName>, boolean>
|
|
156
168
|
): Promise<void> {
|
|
157
|
-
if (!this.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
169
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
170
|
+
// Accept a `"table:id"` string OR a RecordId — live-query rows carry their
|
|
171
|
+
// `id` as a RecordId, so callers can pass `db.delete('game', row.id)`
|
|
172
|
+
// directly. Build the canonical string from the raw id part (not
|
|
173
|
+
// `RecordId.toString()`, which escapes special chars) so it round-trips
|
|
174
|
+
// through the engine's `parseRecordIdString`. InnerQuery selectors are not
|
|
175
|
+
// supported yet. (cross-package RecordId instances → match by constructor name.)
|
|
176
|
+
const isRecordId =
|
|
177
|
+
selector instanceof RecordId || (selector as any)?.constructor?.name === 'RecordId';
|
|
178
|
+
let id: string;
|
|
179
|
+
if (typeof selector === 'string') {
|
|
180
|
+
id = selector;
|
|
181
|
+
} else if (isRecordId) {
|
|
182
|
+
id = `${tableName as string}:${(selector as RecordId).id}`;
|
|
183
|
+
} else {
|
|
184
|
+
throw new Error('Only string ID or RecordId selectors are supported currently with core');
|
|
185
|
+
}
|
|
186
|
+
await this.sp00ky.delete(tableName as string, id);
|
|
161
187
|
}
|
|
162
188
|
|
|
163
189
|
/**
|
|
@@ -165,9 +191,9 @@ export class SyncedDb<S extends SchemaStructure> {
|
|
|
165
191
|
*/
|
|
166
192
|
public query<TName extends TableNames<S>>(
|
|
167
193
|
table: TName
|
|
168
|
-
): QueryBuilder<S, TName,
|
|
169
|
-
if (!this.
|
|
170
|
-
return this.
|
|
194
|
+
): QueryBuilder<S, TName, Sp00kyQueryResultPromise, {}, false> {
|
|
195
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
196
|
+
return this.sp00ky.query(table, {});
|
|
171
197
|
}
|
|
172
198
|
|
|
173
199
|
/**
|
|
@@ -182,17 +208,17 @@ export class SyncedDb<S extends SchemaStructure> {
|
|
|
182
208
|
payload: RoutePayload<S, B, R>,
|
|
183
209
|
options?: RunOptions,
|
|
184
210
|
): Promise<void> {
|
|
185
|
-
if (!this.
|
|
186
|
-
await this.
|
|
211
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
212
|
+
await this.sp00ky.run(backend, path, payload, options);
|
|
187
213
|
}
|
|
188
214
|
|
|
189
215
|
/**
|
|
190
216
|
* Authenticate with the database
|
|
191
217
|
*/
|
|
192
218
|
public async authenticate(token: string): Promise<RecordId<string>> {
|
|
193
|
-
|
|
194
|
-
//
|
|
195
|
-
// Wait, checked
|
|
219
|
+
await this.sp00ky?.authenticate(token);
|
|
220
|
+
// Sp00kyClient.authenticate returns whatever remote.authenticate returns (boolean or token usually?)
|
|
221
|
+
// Wait, checked Sp00kyClient: return this.remote.getClient().authenticate(token);
|
|
196
222
|
// SurrealDB authenticate returns void? or token?
|
|
197
223
|
// Assuming void or token.
|
|
198
224
|
return new RecordId('user', 'me'); // Placeholder or actual?
|
|
@@ -210,54 +236,76 @@ export class SyncedDb<S extends SchemaStructure> {
|
|
|
210
236
|
* Sign out, clear session and local storage
|
|
211
237
|
*/
|
|
212
238
|
public async signOut(): Promise<void> {
|
|
213
|
-
if (!this.
|
|
214
|
-
await this.
|
|
239
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
240
|
+
await this.sp00ky.auth.signOut();
|
|
215
241
|
}
|
|
216
242
|
|
|
217
243
|
/**
|
|
218
244
|
* Execute a function with direct access to the remote database connection
|
|
219
245
|
*/
|
|
220
246
|
public async useRemote<T>(fn: (db: Surreal) => T | Promise<T>): Promise<T> {
|
|
221
|
-
if (!this.
|
|
222
|
-
return await this.
|
|
247
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
248
|
+
return await this.sp00ky.useRemote(fn);
|
|
223
249
|
}
|
|
224
250
|
/**
|
|
225
251
|
* Access the remote database service directly
|
|
226
252
|
*/
|
|
227
|
-
get remote():
|
|
228
|
-
if (!this.
|
|
229
|
-
return this.
|
|
253
|
+
get remote(): Sp00kyClient<S>['remoteClient'] {
|
|
254
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
255
|
+
return this.sp00ky.remoteClient;
|
|
230
256
|
}
|
|
231
257
|
|
|
232
258
|
/**
|
|
233
259
|
* Access the local database service directly
|
|
234
260
|
*/
|
|
235
|
-
get local():
|
|
236
|
-
if (!this.
|
|
237
|
-
return this.
|
|
261
|
+
get local(): Sp00kyClient<S>['localClient'] {
|
|
262
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
263
|
+
return this.sp00ky.localClient;
|
|
238
264
|
}
|
|
239
265
|
|
|
240
266
|
/**
|
|
241
267
|
* Access the auth service
|
|
242
268
|
*/
|
|
243
269
|
get auth(): AuthService<S> {
|
|
244
|
-
if (!this.
|
|
245
|
-
return this.
|
|
270
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
271
|
+
return this.sp00ky.auth;
|
|
246
272
|
}
|
|
247
273
|
|
|
248
274
|
get pendingMutationCount(): number {
|
|
249
|
-
if (!this.
|
|
250
|
-
return this.
|
|
275
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
276
|
+
return this.sp00ky.pendingMutationCount;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/** Diagnostic — see `Sp00kyClient.liveRetryCount`. */
|
|
280
|
+
get liveRetryCount(): number {
|
|
281
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
282
|
+
return this.sp00ky.liveRetryCount;
|
|
251
283
|
}
|
|
252
284
|
|
|
253
285
|
subscribeToPendingMutations(cb: (count: number) => void): () => void {
|
|
254
|
-
if (!this.
|
|
255
|
-
return this.
|
|
286
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
287
|
+
return this.sp00ky.subscribeToPendingMutations(cb);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Current sync-health snapshot. See {@link useSyncStatus}. */
|
|
291
|
+
get syncHealth(): SyncHealth {
|
|
292
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
293
|
+
return this.sp00ky.syncHealth;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Observe sync health. Fires immediately with the current status and again
|
|
298
|
+
* on every healthy↔degraded transition. Prefer the `useSyncStatus` hook in
|
|
299
|
+
* components; this is the imperative escape hatch.
|
|
300
|
+
*/
|
|
301
|
+
subscribeToSyncHealth(cb: (health: SyncHealth) => void): () => void {
|
|
302
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
303
|
+
return this.sp00ky.subscribeToSyncHealth(cb);
|
|
256
304
|
}
|
|
257
305
|
|
|
258
306
|
bucket<B extends BucketNames<S>>(name: B): BucketHandle {
|
|
259
|
-
if (!this.
|
|
260
|
-
return this.
|
|
307
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
308
|
+
return this.sp00ky.bucket(name);
|
|
261
309
|
}
|
|
262
310
|
|
|
263
311
|
getBucketConfig(name: string): BucketDefinitionSchema | undefined {
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { JSX} from 'solid-js';
|
|
2
|
+
import { createSignal, onMount, createComponent, createMemo, mergeProps } from 'solid-js';
|
|
2
3
|
import type { SchemaStructure } from '@spooky/query-builder';
|
|
3
4
|
import type { SyncedDbConfig } from '../types';
|
|
4
5
|
import { SyncedDb } from '../index';
|
|
5
|
-
import {
|
|
6
|
+
import { Sp00kyContext } from './context';
|
|
6
7
|
|
|
7
|
-
export interface
|
|
8
|
+
export interface Sp00kyProviderProps<S extends SchemaStructure> {
|
|
8
9
|
config: SyncedDbConfig<S>;
|
|
9
10
|
fallback?: JSX.Element;
|
|
10
11
|
onError?: (error: Error) => void;
|
|
@@ -12,8 +13,8 @@ export interface SpookyProviderProps<S extends SchemaStructure> {
|
|
|
12
13
|
children: JSX.Element;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
|
-
export function
|
|
16
|
-
props:
|
|
16
|
+
export function Sp00kyProvider<S extends SchemaStructure>(
|
|
17
|
+
props: Sp00kyProviderProps<S>
|
|
17
18
|
): JSX.Element {
|
|
18
19
|
const merged = mergeProps(
|
|
19
20
|
{
|
|
@@ -35,7 +36,8 @@ export function SpookyProvider<S extends SchemaStructure>(
|
|
|
35
36
|
if (merged.onError) {
|
|
36
37
|
merged.onError(error);
|
|
37
38
|
} else {
|
|
38
|
-
|
|
39
|
+
// oxlint-disable-next-line no-console
|
|
40
|
+
console.error('Sp00kyProvider: Failed to initialize database', error);
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
});
|
|
@@ -43,7 +45,7 @@ export function SpookyProvider<S extends SchemaStructure>(
|
|
|
43
45
|
const content = createMemo(() => {
|
|
44
46
|
const instance = db();
|
|
45
47
|
if (!instance) return merged.fallback;
|
|
46
|
-
return createComponent(
|
|
48
|
+
return createComponent(Sp00kyContext.Provider, {
|
|
47
49
|
value: instance,
|
|
48
50
|
get children() {
|
|
49
51
|
return merged.children;
|
package/src/lib/context.ts
CHANGED
|
@@ -2,12 +2,12 @@ import { createContext, useContext } from 'solid-js';
|
|
|
2
2
|
import type { SchemaStructure } from '@spooky/query-builder';
|
|
3
3
|
import type { SyncedDb } from '../index';
|
|
4
4
|
|
|
5
|
-
export const
|
|
5
|
+
export const Sp00kyContext = createContext<SyncedDb<any> | undefined>();
|
|
6
6
|
|
|
7
7
|
export function useDb<S extends SchemaStructure>(): SyncedDb<S> {
|
|
8
|
-
const db = useContext(
|
|
8
|
+
const db = useContext(Sp00kyContext);
|
|
9
9
|
if (!db) {
|
|
10
|
-
throw new Error('useDb must be used within a <
|
|
10
|
+
throw new Error('useDb must be used within a <Sp00kyProvider>. Wrap your app in <Sp00kyProvider config={...}>.');
|
|
11
11
|
}
|
|
12
12
|
return db as SyncedDb<S>;
|
|
13
13
|
}
|
package/src/lib/models.ts
CHANGED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { createEffect, createSignal, onCleanup, useContext, type Accessor } from 'solid-js';
|
|
2
|
+
import { Sp00kyContext } from './context';
|
|
3
|
+
import type { CrdtField } from '@spooky-sync/core';
|
|
4
|
+
|
|
5
|
+
export function useCrdtField(
|
|
6
|
+
table: string,
|
|
7
|
+
recordId: () => string | undefined,
|
|
8
|
+
field: string,
|
|
9
|
+
fallbackText?: () => string | undefined,
|
|
10
|
+
): Accessor<CrdtField | null> {
|
|
11
|
+
const db = useContext(Sp00kyContext);
|
|
12
|
+
if (!db) {
|
|
13
|
+
throw new Error('useCrdtField must be used within a <Sp00kyProvider>');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const [crdtField, setCrdtField] = createSignal<CrdtField | null>(null);
|
|
17
|
+
let currentId: string | undefined;
|
|
18
|
+
let initialized = false;
|
|
19
|
+
|
|
20
|
+
createEffect(() => {
|
|
21
|
+
const id = recordId();
|
|
22
|
+
|
|
23
|
+
// Skip if the ID hasn't changed (but allow the first non-undefined value through)
|
|
24
|
+
if (initialized && id === currentId) return;
|
|
25
|
+
|
|
26
|
+
// Close previous field
|
|
27
|
+
if (currentId && crdtField()) {
|
|
28
|
+
db.getSp00ky().closeCrdtField(table, currentId, field);
|
|
29
|
+
setCrdtField(null);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
currentId = id;
|
|
33
|
+
initialized = true;
|
|
34
|
+
|
|
35
|
+
if (!id) return;
|
|
36
|
+
|
|
37
|
+
const sp00ky = db.getSp00ky();
|
|
38
|
+
const text = fallbackText?.();
|
|
39
|
+
sp00ky
|
|
40
|
+
.openCrdtField(table, id, field, text)
|
|
41
|
+
.then((cf) => {
|
|
42
|
+
if (currentId === id) {
|
|
43
|
+
setCrdtField(cf);
|
|
44
|
+
}
|
|
45
|
+
})
|
|
46
|
+
.catch((err) => {
|
|
47
|
+
// Silent rejections here leave the consumer's `Show when={field()}`
|
|
48
|
+
// permanently stuck on its fallback (typically a static `<p>` with
|
|
49
|
+
// no editing UI), with no error trail. Surface the failure so the
|
|
50
|
+
// root cause (missing `@crdt` annotation, schema codegen drift,
|
|
51
|
+
// local DB query failure, etc.) is visible in the console instead
|
|
52
|
+
// of silently breaking collaborative fields.
|
|
53
|
+
console.error(
|
|
54
|
+
`[useCrdtField] Failed to open CRDT field ${table}.${field} on ${id}:`,
|
|
55
|
+
err,
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
onCleanup(() => {
|
|
61
|
+
if (currentId && crdtField()) {
|
|
62
|
+
db.getSp00ky().closeCrdtField(table, currentId, field);
|
|
63
|
+
setCrdtField(null);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return crdtField;
|
|
68
|
+
}
|
|
@@ -78,9 +78,8 @@ export function useDownloadFile<S extends SchemaStructure>(
|
|
|
78
78
|
|
|
79
79
|
let currentKey: string | null = null;
|
|
80
80
|
let privateUrl: string | null = null;
|
|
81
|
-
let refetchTrigger: () => void;
|
|
82
81
|
const [refetchSignal, setRefetchSignal] = createSignal(0);
|
|
83
|
-
refetchTrigger = () => setRefetchSignal((n) => n + 1);
|
|
82
|
+
const refetchTrigger = () => setRefetchSignal((n) => n + 1);
|
|
84
83
|
|
|
85
84
|
async function doDownload(key: string, filePath: string): Promise<string | null> {
|
|
86
85
|
if (useCache) {
|
|
@@ -184,6 +183,7 @@ export function useDownloadFile<S extends SchemaStructure>(
|
|
|
184
183
|
setUrl(result);
|
|
185
184
|
setIsLoading(false);
|
|
186
185
|
}
|
|
186
|
+
return undefined;
|
|
187
187
|
},
|
|
188
188
|
(err) => {
|
|
189
189
|
if (!cancelled) {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createSignal, onCleanup, type Accessor } from 'solid-js';
|
|
2
|
+
import { useDb } from './context';
|
|
3
|
+
import type { FeatureFlagOptions } from '@spooky-sync/core';
|
|
4
|
+
|
|
5
|
+
export interface UseFeatureFlag {
|
|
6
|
+
variant: Accessor<string | undefined>;
|
|
7
|
+
payload: Accessor<unknown | undefined>;
|
|
8
|
+
enabled: Accessor<boolean>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Subscribe to a feature flag for the currently authenticated user.
|
|
13
|
+
*
|
|
14
|
+
* Returns three Solid accessors that update reactively whenever the
|
|
15
|
+
* server-materialized assignment in `_00_user_feature` changes. Backed by
|
|
16
|
+
* the same SSP + sync pipeline that powers `useQuery`, so toggling a flag
|
|
17
|
+
* via `spky flag enable <key>` propagates to the UI without a refresh.
|
|
18
|
+
*
|
|
19
|
+
* `enabled()` is `true` when the resolved variant exists and is not 'off'.
|
|
20
|
+
* For multi-variant flags, prefer `variant()` directly.
|
|
21
|
+
*/
|
|
22
|
+
export function useFeatureFlag(
|
|
23
|
+
key: string,
|
|
24
|
+
options?: FeatureFlagOptions,
|
|
25
|
+
): UseFeatureFlag {
|
|
26
|
+
const db = useDb();
|
|
27
|
+
const handle = db.getSp00ky().feature(key, options);
|
|
28
|
+
|
|
29
|
+
const [variant, setVariant] = createSignal<string | undefined>(handle.variant());
|
|
30
|
+
const [payload, setPayload] = createSignal<unknown | undefined>(handle.payload());
|
|
31
|
+
|
|
32
|
+
const unsub = handle.subscribe((s) => {
|
|
33
|
+
setVariant(s.variant ?? options?.fallback);
|
|
34
|
+
setPayload(s.payload);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
onCleanup(() => {
|
|
38
|
+
unsub();
|
|
39
|
+
handle.close();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
variant,
|
|
44
|
+
payload,
|
|
45
|
+
enabled: () => {
|
|
46
|
+
const v = variant();
|
|
47
|
+
return v !== undefined && v !== 'off';
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -33,6 +33,7 @@ export function useFileUpload<S extends SchemaStructure>(
|
|
|
33
33
|
bucketName = dbOrBucketName as BucketNames<S>;
|
|
34
34
|
} else {
|
|
35
35
|
db = dbOrBucketName as SyncedDb<S>;
|
|
36
|
+
// oxlint-disable-next-line no-non-null-assertion
|
|
36
37
|
bucketName = maybeBucketName!;
|
|
37
38
|
}
|
|
38
39
|
|
|
@@ -52,7 +53,7 @@ export function useFileUpload<S extends SchemaStructure>(
|
|
|
52
53
|
const config = db.getBucketConfig(bucketName as string);
|
|
53
54
|
if (!config) return;
|
|
54
55
|
|
|
55
|
-
if (config.maxSize
|
|
56
|
+
if (config.maxSize !== null && config.maxSize !== undefined && file.size > config.maxSize) {
|
|
56
57
|
const maxMB = (config.maxSize / (1024 * 1024)).toFixed(1);
|
|
57
58
|
throw new Error(`File exceeds maximum size of ${maxMB} MB.`);
|
|
58
59
|
}
|