@spooky-sync/client-solid 0.0.1-canary.8 → 0.0.1-canary.80
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 +177 -65
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +57 -21
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +57 -21
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +176 -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 +84 -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/types/index.ts +3 -4
package/src/index.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
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
9
|
} from '@spooky-sync/core';
|
|
10
10
|
|
|
11
|
-
import {
|
|
11
|
+
import type {
|
|
12
12
|
GetTable,
|
|
13
13
|
QueryBuilder,
|
|
14
14
|
SchemaStructure,
|
|
@@ -25,19 +25,28 @@ import {
|
|
|
25
25
|
RoutePayload,
|
|
26
26
|
BucketNames,
|
|
27
27
|
BucketDefinitionSchema,
|
|
28
|
+
QueryModifier,
|
|
29
|
+
QueryModifierBuilder,
|
|
30
|
+
QueryInfo,
|
|
31
|
+
RelationshipsMetadata,
|
|
32
|
+
RelationshipDefinition,
|
|
33
|
+
InferRelatedModelFromMetadata,
|
|
34
|
+
GetCardinality,
|
|
28
35
|
} from '@spooky-sync/query-builder';
|
|
29
36
|
|
|
30
|
-
import { RecordId, Uuid, Surreal } from 'surrealdb';
|
|
37
|
+
import { RecordId, Uuid, type Surreal } from 'surrealdb';
|
|
31
38
|
export { RecordId, Uuid };
|
|
32
39
|
export type { Model, GenericModel, GenericSchema, ModelPayload } from './lib/models';
|
|
33
40
|
export { useQuery } from './lib/use-query';
|
|
41
|
+
export { useCrdtField } from './lib/use-crdt-field';
|
|
42
|
+
export { useFeatureFlag, type UseFeatureFlag } from './lib/use-feature-flag';
|
|
34
43
|
export { useFileUpload, type FileUploadResult } from './lib/use-file-upload';
|
|
35
44
|
export { useDownloadFile, type UseDownloadFileOptions, type UseDownloadFileResult } from './lib/use-download-file';
|
|
36
|
-
export {
|
|
45
|
+
export { Sp00kyProvider, type Sp00kyProviderProps } from './lib/Sp00kyProvider';
|
|
37
46
|
export { useDb } from './lib/context';
|
|
38
47
|
|
|
39
48
|
// export { AuthEventTypes } from "@spooky-sync/core"; // TODO: Verify if AuthEventTypes exists in core
|
|
40
|
-
|
|
49
|
+
|
|
41
50
|
|
|
42
51
|
// Re-export query builder types for convenience
|
|
43
52
|
export type {
|
|
@@ -52,7 +61,7 @@ export type {
|
|
|
52
61
|
TableModel,
|
|
53
62
|
TableNames,
|
|
54
63
|
QueryResult,
|
|
55
|
-
}
|
|
64
|
+
};
|
|
56
65
|
|
|
57
66
|
export type RelationshipField<
|
|
58
67
|
Schema extends SchemaStructure,
|
|
@@ -94,30 +103,30 @@ export type WithRelatedMany<Field extends string, RelatedFields extends RelatedF
|
|
|
94
103
|
};
|
|
95
104
|
|
|
96
105
|
/**
|
|
97
|
-
* SyncedDb - A thin wrapper around
|
|
98
|
-
* Delegates all logic to the underlying
|
|
106
|
+
* SyncedDb - A thin wrapper around sp00ky-ts for Solid.js integration
|
|
107
|
+
* Delegates all logic to the underlying sp00ky-ts instance
|
|
99
108
|
*/
|
|
100
109
|
export class SyncedDb<S extends SchemaStructure> {
|
|
101
110
|
private config: SyncedDbConfig<S>;
|
|
102
|
-
private
|
|
111
|
+
private sp00ky: Sp00kyClient<S> | null = null;
|
|
103
112
|
private _initialized = false;
|
|
104
113
|
|
|
105
114
|
constructor(config: SyncedDbConfig<S>) {
|
|
106
115
|
this.config = config;
|
|
107
116
|
}
|
|
108
117
|
|
|
109
|
-
public
|
|
110
|
-
if (!this.
|
|
111
|
-
return this.
|
|
118
|
+
public getSp00ky(): Sp00kyClient<S> {
|
|
119
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
120
|
+
return this.sp00ky;
|
|
112
121
|
}
|
|
113
122
|
|
|
114
123
|
/**
|
|
115
|
-
* Initialize the
|
|
124
|
+
* Initialize the sp00ky-ts instance
|
|
116
125
|
*/
|
|
117
126
|
async init(): Promise<void> {
|
|
118
127
|
if (this._initialized) return;
|
|
119
|
-
this.
|
|
120
|
-
await this.
|
|
128
|
+
this.sp00ky = new Sp00kyClient<S>(this.config);
|
|
129
|
+
await this.sp00ky.init();
|
|
121
130
|
this._initialized = true;
|
|
122
131
|
}
|
|
123
132
|
|
|
@@ -125,8 +134,8 @@ export class SyncedDb<S extends SchemaStructure> {
|
|
|
125
134
|
* Create a new record in the database
|
|
126
135
|
*/
|
|
127
136
|
async create(id: string, payload: Record<string, unknown>): Promise<void> {
|
|
128
|
-
if (!this.
|
|
129
|
-
await this.
|
|
137
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
138
|
+
await this.sp00ky.create(id, payload as Record<string, unknown>);
|
|
130
139
|
}
|
|
131
140
|
|
|
132
141
|
/**
|
|
@@ -138,8 +147,8 @@ export class SyncedDb<S extends SchemaStructure> {
|
|
|
138
147
|
payload: Partial<TableModel<GetTable<S, TName>>>,
|
|
139
148
|
options?: UpdateOptions
|
|
140
149
|
): Promise<void> {
|
|
141
|
-
if (!this.
|
|
142
|
-
await this.
|
|
150
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
151
|
+
await this.sp00ky.update(
|
|
143
152
|
tableName as string,
|
|
144
153
|
recordId,
|
|
145
154
|
payload as Record<string, unknown>,
|
|
@@ -152,12 +161,26 @@ export class SyncedDb<S extends SchemaStructure> {
|
|
|
152
161
|
*/
|
|
153
162
|
async delete<TName extends TableNames<S>>(
|
|
154
163
|
tableName: TName,
|
|
155
|
-
selector: string | InnerQuery<GetTable<S, TName>, boolean>
|
|
164
|
+
selector: string | RecordId | InnerQuery<GetTable<S, TName>, boolean>
|
|
156
165
|
): Promise<void> {
|
|
157
|
-
if (!this.
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
166
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
167
|
+
// Accept a `"table:id"` string OR a RecordId — live-query rows carry their
|
|
168
|
+
// `id` as a RecordId, so callers can pass `db.delete('game', row.id)`
|
|
169
|
+
// directly. Build the canonical string from the raw id part (not
|
|
170
|
+
// `RecordId.toString()`, which escapes special chars) so it round-trips
|
|
171
|
+
// through the engine's `parseRecordIdString`. InnerQuery selectors are not
|
|
172
|
+
// supported yet. (cross-package RecordId instances → match by constructor name.)
|
|
173
|
+
const isRecordId =
|
|
174
|
+
selector instanceof RecordId || (selector as any)?.constructor?.name === 'RecordId';
|
|
175
|
+
let id: string;
|
|
176
|
+
if (typeof selector === 'string') {
|
|
177
|
+
id = selector;
|
|
178
|
+
} else if (isRecordId) {
|
|
179
|
+
id = `${tableName as string}:${(selector as RecordId).id}`;
|
|
180
|
+
} else {
|
|
181
|
+
throw new Error('Only string ID or RecordId selectors are supported currently with core');
|
|
182
|
+
}
|
|
183
|
+
await this.sp00ky.delete(tableName as string, id);
|
|
161
184
|
}
|
|
162
185
|
|
|
163
186
|
/**
|
|
@@ -165,9 +188,9 @@ export class SyncedDb<S extends SchemaStructure> {
|
|
|
165
188
|
*/
|
|
166
189
|
public query<TName extends TableNames<S>>(
|
|
167
190
|
table: TName
|
|
168
|
-
): QueryBuilder<S, TName,
|
|
169
|
-
if (!this.
|
|
170
|
-
return this.
|
|
191
|
+
): QueryBuilder<S, TName, Sp00kyQueryResultPromise, {}, false> {
|
|
192
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
193
|
+
return this.sp00ky.query(table, {});
|
|
171
194
|
}
|
|
172
195
|
|
|
173
196
|
/**
|
|
@@ -182,17 +205,17 @@ export class SyncedDb<S extends SchemaStructure> {
|
|
|
182
205
|
payload: RoutePayload<S, B, R>,
|
|
183
206
|
options?: RunOptions,
|
|
184
207
|
): Promise<void> {
|
|
185
|
-
if (!this.
|
|
186
|
-
await this.
|
|
208
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
209
|
+
await this.sp00ky.run(backend, path, payload, options);
|
|
187
210
|
}
|
|
188
211
|
|
|
189
212
|
/**
|
|
190
213
|
* Authenticate with the database
|
|
191
214
|
*/
|
|
192
215
|
public async authenticate(token: string): Promise<RecordId<string>> {
|
|
193
|
-
|
|
194
|
-
//
|
|
195
|
-
// Wait, checked
|
|
216
|
+
await this.sp00ky?.authenticate(token);
|
|
217
|
+
// Sp00kyClient.authenticate returns whatever remote.authenticate returns (boolean or token usually?)
|
|
218
|
+
// Wait, checked Sp00kyClient: return this.remote.getClient().authenticate(token);
|
|
196
219
|
// SurrealDB authenticate returns void? or token?
|
|
197
220
|
// Assuming void or token.
|
|
198
221
|
return new RecordId('user', 'me'); // Placeholder or actual?
|
|
@@ -210,54 +233,60 @@ export class SyncedDb<S extends SchemaStructure> {
|
|
|
210
233
|
* Sign out, clear session and local storage
|
|
211
234
|
*/
|
|
212
235
|
public async signOut(): Promise<void> {
|
|
213
|
-
if (!this.
|
|
214
|
-
await this.
|
|
236
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
237
|
+
await this.sp00ky.auth.signOut();
|
|
215
238
|
}
|
|
216
239
|
|
|
217
240
|
/**
|
|
218
241
|
* Execute a function with direct access to the remote database connection
|
|
219
242
|
*/
|
|
220
243
|
public async useRemote<T>(fn: (db: Surreal) => T | Promise<T>): Promise<T> {
|
|
221
|
-
if (!this.
|
|
222
|
-
return await this.
|
|
244
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
245
|
+
return await this.sp00ky.useRemote(fn);
|
|
223
246
|
}
|
|
224
247
|
/**
|
|
225
248
|
* Access the remote database service directly
|
|
226
249
|
*/
|
|
227
|
-
get remote():
|
|
228
|
-
if (!this.
|
|
229
|
-
return this.
|
|
250
|
+
get remote(): Sp00kyClient<S>['remoteClient'] {
|
|
251
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
252
|
+
return this.sp00ky.remoteClient;
|
|
230
253
|
}
|
|
231
254
|
|
|
232
255
|
/**
|
|
233
256
|
* Access the local database service directly
|
|
234
257
|
*/
|
|
235
|
-
get local():
|
|
236
|
-
if (!this.
|
|
237
|
-
return this.
|
|
258
|
+
get local(): Sp00kyClient<S>['localClient'] {
|
|
259
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
260
|
+
return this.sp00ky.localClient;
|
|
238
261
|
}
|
|
239
262
|
|
|
240
263
|
/**
|
|
241
264
|
* Access the auth service
|
|
242
265
|
*/
|
|
243
266
|
get auth(): AuthService<S> {
|
|
244
|
-
if (!this.
|
|
245
|
-
return this.
|
|
267
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
268
|
+
return this.sp00ky.auth;
|
|
246
269
|
}
|
|
247
270
|
|
|
248
271
|
get pendingMutationCount(): number {
|
|
249
|
-
if (!this.
|
|
250
|
-
return this.
|
|
272
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
273
|
+
return this.sp00ky.pendingMutationCount;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Diagnostic — see `Sp00kyClient.liveRetryCount`. */
|
|
277
|
+
get liveRetryCount(): number {
|
|
278
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
279
|
+
return this.sp00ky.liveRetryCount;
|
|
251
280
|
}
|
|
252
281
|
|
|
253
282
|
subscribeToPendingMutations(cb: (count: number) => void): () => void {
|
|
254
|
-
if (!this.
|
|
255
|
-
return this.
|
|
283
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
284
|
+
return this.sp00ky.subscribeToPendingMutations(cb);
|
|
256
285
|
}
|
|
257
286
|
|
|
258
287
|
bucket<B extends BucketNames<S>>(name: B): BucketHandle {
|
|
259
|
-
if (!this.
|
|
260
|
-
return this.
|
|
288
|
+
if (!this.sp00ky) throw new Error('SyncedDb not initialized');
|
|
289
|
+
return this.sp00ky.bucket(name);
|
|
261
290
|
}
|
|
262
291
|
|
|
263
292
|
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
|
}
|