@spooky-sync/client-solid2 0.0.1-canary.200

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,57 @@
1
+ import { createEffect, createSignal, type Accessor } from 'solid-js';
2
+ import { useDb } 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 = useDb();
12
+ const [crdtField, setCrdtField] = createSignal<CrdtField | null>(null, { ownedWrite: true });
13
+
14
+ // Two-arg Solid 2 effect: the compute tracks `recordId`, the apply owns the
15
+ // open/close lifecycle and returns the cleanup — which runs both when the id
16
+ // changes (before the next apply) and on unmount. That replaces the Solid 1
17
+ // version's manual currentId/initialized bookkeeping.
18
+ createEffect(
19
+ () => recordId(),
20
+ (id) => {
21
+ if (!id) {
22
+ setCrdtField(null);
23
+ return;
24
+ }
25
+ const sp00ky = db.getSp00ky();
26
+ let superseded = false;
27
+ const text = fallbackText?.();
28
+ sp00ky
29
+ .openCrdtField(table, id, field, text)
30
+ .then((cf) => {
31
+ if (!superseded) {
32
+ setCrdtField(cf);
33
+ } else {
34
+ sp00ky.closeCrdtField(table, id, field);
35
+ }
36
+ })
37
+ .catch((err) => {
38
+ // Silent rejections here leave the consumer's `Show when={field()}`
39
+ // permanently stuck on its fallback (typically a static `<p>` with
40
+ // no editing UI), with no error trail. Surface the failure so the
41
+ // root cause (missing `@crdt` annotation, schema codegen drift,
42
+ // local DB query failure, etc.) is visible in the console instead
43
+ // of silently breaking collaborative fields.
44
+ console.error(`[useCrdtField] Failed to open CRDT field ${table}.${field} on ${id}:`, err);
45
+ });
46
+ return () => {
47
+ superseded = true;
48
+ if (crdtField()) {
49
+ sp00ky.closeCrdtField(table, id, field);
50
+ setCrdtField(null);
51
+ }
52
+ };
53
+ }
54
+ );
55
+
56
+ return crdtField;
57
+ }
@@ -0,0 +1,181 @@
1
+ import { createSignal, createEffect, onCleanup, type Accessor } from 'solid-js';
2
+ import type { SchemaStructure, BucketNames } from '@spooky-sync/query-builder';
3
+ import type { BlobUrlLease } from '@spooky-sync/core';
4
+ import type { SyncedDb } from '../index';
5
+ import { useDb } from './context';
6
+
7
+ export interface UseDownloadFileOptions {
8
+ /**
9
+ * Master switch, default `true`. `false` gives every hook instance its own
10
+ * private object URL fetched fresh from the bucket and revoked on unmount —
11
+ * no sharing, no persistence, no reuse.
12
+ */
13
+ cache?: boolean;
14
+ /**
15
+ * Keep the bytes in OPFS so they survive a reload and are available offline.
16
+ * Default `true`. Turn off for one-shot or sensitive files; the in-tab object
17
+ * URL is still shared between components rendering the same path.
18
+ */
19
+ persist?: boolean;
20
+ /** Exempt this file from pressure eviction. Pinned bytes never expire. */
21
+ pin?: boolean;
22
+ /**
23
+ * `'never'` (default) treats a bucket path as immutable, which is how paths
24
+ * are written (`crypto.randomUUID() + ext`). `'head'` spends a remote `head()`
25
+ * to compare sizes before trusting the cached copy — for paths the app
26
+ * overwrites in place.
27
+ */
28
+ revalidate?: 'never' | 'head';
29
+ }
30
+
31
+ export interface UseDownloadFileResult {
32
+ url: Accessor<string | null>;
33
+ isLoading: Accessor<boolean>;
34
+ error: Accessor<Error | null>;
35
+ refetch: () => void;
36
+ }
37
+
38
+ export function useDownloadFile<S extends SchemaStructure>(
39
+ bucketName: BucketNames<S>,
40
+ path: Accessor<string | null | undefined>,
41
+ options?: UseDownloadFileOptions
42
+ ): UseDownloadFileResult;
43
+ export function useDownloadFile<S extends SchemaStructure>(
44
+ db: SyncedDb<S>,
45
+ bucketName: BucketNames<S>,
46
+ path: Accessor<string | null | undefined>,
47
+ options?: UseDownloadFileOptions
48
+ ): UseDownloadFileResult;
49
+ export function useDownloadFile<S extends SchemaStructure>(
50
+ dbOrBucketName: SyncedDb<S> | BucketNames<S>,
51
+ bucketNameOrPath?: BucketNames<S> | Accessor<string | null | undefined>,
52
+ pathOrOptions?: Accessor<string | null | undefined> | UseDownloadFileOptions,
53
+ maybeOptions?: UseDownloadFileOptions
54
+ ): UseDownloadFileResult {
55
+ let db: SyncedDb<S>;
56
+ let bucketName: BucketNames<S>;
57
+ let path: Accessor<string | null | undefined>;
58
+ let options: UseDownloadFileOptions;
59
+
60
+ if (typeof dbOrBucketName === 'string') {
61
+ db = useDb<S>();
62
+ bucketName = dbOrBucketName as BucketNames<S>;
63
+ path = bucketNameOrPath as Accessor<string | null | undefined>;
64
+ options = (pathOrOptions as UseDownloadFileOptions) ?? {};
65
+ } else {
66
+ db = dbOrBucketName as SyncedDb<S>;
67
+ bucketName = bucketNameOrPath as BucketNames<S>;
68
+ path = pathOrOptions as Accessor<string | null | undefined>;
69
+ options = maybeOptions ?? {};
70
+ }
71
+
72
+ const useCache = options.cache !== false;
73
+
74
+ // Written from fetch continuations — outside any tracking scope.
75
+ const [url, setUrl] = createSignal<string | null>(null, { ownedWrite: true });
76
+ const [isLoading, setIsLoading] = createSignal(false, { ownedWrite: true });
77
+ const [error, setError] = createSignal<Error | null>(null, { ownedWrite: true });
78
+
79
+ // Exactly one of these is held at a time: a refcounted lease on the shared
80
+ // cache entry, or a private URL this instance minted and must revoke itself.
81
+ let lease: BlobUrlLease | null = null;
82
+ let privateUrl: string | null = null;
83
+
84
+ const [refetchSignal, setRefetchSignal] = createSignal(0);
85
+ /** Consumed by the next effect run, so `refetch()` bypasses every layer once. */
86
+ let reloadOnce = false;
87
+
88
+ function releaseCurrent() {
89
+ lease?.release();
90
+ lease = null;
91
+ if (privateUrl) {
92
+ URL.revokeObjectURL(privateUrl);
93
+ privateUrl = null;
94
+ }
95
+ }
96
+
97
+ // Two-arg Solid 2 effect: compute tracks path + refetch tick; apply runs the
98
+ // fetch and returns the cancel/release cleanup, which runs before the next
99
+ // apply and on unmount.
100
+ createEffect(
101
+ () => {
102
+ refetchSignal();
103
+ return path();
104
+ },
105
+ (filePath) => {
106
+ releaseCurrent();
107
+
108
+ if (!filePath) {
109
+ setUrl(null);
110
+ setIsLoading(false);
111
+ setError(null);
112
+ return;
113
+ }
114
+
115
+ const reload = reloadOnce;
116
+ reloadOnce = false;
117
+
118
+ let cancelled = false;
119
+ setIsLoading(true);
120
+ setError(null);
121
+
122
+ const bucket = db.bucket(bucketName);
123
+ const resolve = useCache
124
+ ? bucket
125
+ .url(filePath, {
126
+ persist: options.persist !== false,
127
+ pin: options.pin,
128
+ revalidate: options.revalidate,
129
+ reload,
130
+ })
131
+ .then((acquired) => {
132
+ if (!acquired) return null;
133
+ if (cancelled) {
134
+ // Unmounted or the path changed mid-flight — hand the reference
135
+ // straight back, or the entry never drops to zero and its object
136
+ // URL leaks for the life of the tab.
137
+ acquired.release();
138
+ return null;
139
+ }
140
+ lease = acquired;
141
+ return acquired.url;
142
+ })
143
+ : bucket.read(filePath, { persist: false, reload: true }).then((blob) => {
144
+ if (!blob || cancelled) return null;
145
+ privateUrl = URL.createObjectURL(blob);
146
+ return privateUrl;
147
+ });
148
+
149
+ resolve.then(
150
+ (result) => {
151
+ if (!cancelled) {
152
+ setUrl(result);
153
+ setIsLoading(false);
154
+ }
155
+ return undefined;
156
+ },
157
+ (err) => {
158
+ if (!cancelled) {
159
+ setError(err instanceof Error ? err : new Error(String(err)));
160
+ setIsLoading(false);
161
+ }
162
+ }
163
+ );
164
+
165
+ return () => {
166
+ cancelled = true;
167
+ };
168
+ }
169
+ );
170
+
171
+ onCleanup(() => {
172
+ releaseCurrent();
173
+ });
174
+
175
+ const refetch = () => {
176
+ reloadOnce = true;
177
+ setRefetchSignal((n) => n + 1);
178
+ };
179
+
180
+ return { url, isLoading, error, refetch };
181
+ }
@@ -0,0 +1,43 @@
1
+ import type { Accessor } from 'solid-js';
2
+ import { onCleanup } from 'solid-js';
3
+ import { useDb } from './context';
4
+ import { fromSubscription } from './from-subscription';
5
+ import type { FeatureFlagOptions } from '@spooky-sync/core';
6
+
7
+ export interface UseFeatureFlag {
8
+ variant: Accessor<string | undefined>;
9
+ payload: Accessor<unknown | undefined>;
10
+ enabled: Accessor<boolean>;
11
+ }
12
+
13
+ /**
14
+ * Subscribe to a feature flag for the currently authenticated user.
15
+ *
16
+ * Returns three Solid accessors that update reactively whenever the
17
+ * server-materialized assignment in `_00_user_feature` changes. Backed by
18
+ * the same SSP + sync pipeline that powers `createQuery`, so toggling a flag
19
+ * via `spky flag enable <key>` propagates to the UI without a refresh.
20
+ *
21
+ * `enabled()` is `true` when the resolved variant exists and is not 'off'.
22
+ * For multi-variant flags, prefer `variant()` directly.
23
+ */
24
+ export function useFeatureFlag(key: string, options?: FeatureFlagOptions): UseFeatureFlag {
25
+ const db = useDb();
26
+ const handle = db.getSp00ky().feature(key, options);
27
+ onCleanup(() => handle.close());
28
+
29
+ const state = fromSubscription<{ variant: string | undefined; payload: unknown }>(
30
+ (cb) =>
31
+ handle.subscribe((s) => cb({ variant: s.variant ?? options?.fallback, payload: s.payload })),
32
+ { variant: handle.variant(), payload: handle.payload() }
33
+ );
34
+
35
+ return {
36
+ variant: () => state().variant,
37
+ payload: () => state().payload,
38
+ enabled: () => {
39
+ const v = state().variant;
40
+ return v !== undefined && v !== 'off';
41
+ },
42
+ };
43
+ }
@@ -0,0 +1,146 @@
1
+ import { createSignal, onCleanup } from 'solid-js';
2
+ import type { SchemaStructure, BucketNames } from '@spooky-sync/query-builder';
3
+ import { fileToUint8Array } from '@spooky-sync/core';
4
+ import type { BucketPutOptions, BucketPutResult } from '@spooky-sync/core';
5
+ import type { SyncedDb } from '../index';
6
+ import { useDb } from './context';
7
+
8
+ export interface FileUploadResult {
9
+ isUploading: () => boolean;
10
+ error: () => Error | null;
11
+ clearError: () => void;
12
+ upload: (
13
+ path: string,
14
+ file: File | Blob,
15
+ options?: BucketPutOptions
16
+ ) => Promise<BucketPutResult | void>;
17
+ download: (path: string) => Promise<string | null>;
18
+ remove: (path: string) => Promise<void>;
19
+ exists: (path: string) => Promise<boolean>;
20
+ }
21
+
22
+ export function useFileUpload<S extends SchemaStructure>(
23
+ bucketName: BucketNames<S>
24
+ ): FileUploadResult;
25
+ export function useFileUpload<S extends SchemaStructure>(
26
+ db: SyncedDb<S>,
27
+ bucketName: BucketNames<S>
28
+ ): FileUploadResult;
29
+ export function useFileUpload<S extends SchemaStructure>(
30
+ dbOrBucketName: SyncedDb<S> | BucketNames<S>,
31
+ maybeBucketName?: BucketNames<S>
32
+ ): FileUploadResult {
33
+ let db: SyncedDb<S>;
34
+ let bucketName: BucketNames<S>;
35
+
36
+ if (typeof dbOrBucketName === 'string') {
37
+ db = useDb<S>();
38
+ bucketName = dbOrBucketName as BucketNames<S>;
39
+ } else {
40
+ db = dbOrBucketName as SyncedDb<S>;
41
+ // oxlint-disable-next-line no-non-null-assertion
42
+ bucketName = maybeBucketName!;
43
+ }
44
+
45
+ // Written from async continuations — outside any tracking scope.
46
+ const [isUploading, setIsUploading] = createSignal(false, { ownedWrite: true });
47
+ const [error, setError] = createSignal<Error | null>(null, { ownedWrite: true });
48
+
49
+ const objectUrls: string[] = [];
50
+ onCleanup(() => {
51
+ for (const url of objectUrls) {
52
+ URL.revokeObjectURL(url);
53
+ }
54
+ });
55
+
56
+ const clearError = () => setError(null);
57
+
58
+ const validate = (file: File | Blob): void => {
59
+ const config = db.getBucketConfig(bucketName as string);
60
+ if (!config) return;
61
+
62
+ if (config.maxSize !== null && config.maxSize !== undefined && file.size > config.maxSize) {
63
+ const maxMB = (config.maxSize / (1024 * 1024)).toFixed(1);
64
+ throw new Error(`File exceeds maximum size of ${maxMB} MB.`);
65
+ }
66
+
67
+ if (config.allowedExtensions && config.allowedExtensions.length > 0) {
68
+ const fileName = (file as File).name;
69
+ if (fileName) {
70
+ const ext = fileName.split('.').pop()?.toLowerCase();
71
+ if (!ext || !config.allowedExtensions.includes(ext)) {
72
+ throw new Error(
73
+ `File type not allowed. Accepted: ${config.allowedExtensions.join(', ')}.`
74
+ );
75
+ }
76
+ }
77
+ }
78
+ };
79
+
80
+ const upload = async (
81
+ path: string,
82
+ file: File | Blob,
83
+ options?: BucketPutOptions
84
+ ): Promise<BucketPutResult | void> => {
85
+ setError(null);
86
+ try {
87
+ validate(file);
88
+ } catch (e) {
89
+ setError(e instanceof Error ? e : new Error(String(e)));
90
+ return;
91
+ }
92
+
93
+ setIsUploading(true);
94
+ try {
95
+ const bytes = await fileToUint8Array(file);
96
+ return await db.bucket(bucketName).put(path, bytes, options);
97
+ } catch (e) {
98
+ setError(e instanceof Error ? e : new Error(String(e)));
99
+ } finally {
100
+ setIsUploading(false);
101
+ }
102
+ };
103
+
104
+ const download = async (path: string): Promise<string | null> => {
105
+ setError(null);
106
+ try {
107
+ const content = await db.bucket(bucketName).get(path);
108
+ if (!content) return null;
109
+ const objectUrl = URL.createObjectURL(new Blob([content as BlobPart]));
110
+ objectUrls.push(objectUrl);
111
+ return objectUrl;
112
+ } catch (e) {
113
+ setError(e instanceof Error ? e : new Error(String(e)));
114
+ return null;
115
+ }
116
+ };
117
+
118
+ const remove = async (path: string): Promise<void> => {
119
+ setError(null);
120
+ try {
121
+ await db.bucket(bucketName).delete(path);
122
+ } catch (e) {
123
+ setError(e instanceof Error ? e : new Error(String(e)));
124
+ }
125
+ };
126
+
127
+ const exists = async (path: string): Promise<boolean> => {
128
+ setError(null);
129
+ try {
130
+ return await db.bucket(bucketName).exists(path);
131
+ } catch (e) {
132
+ setError(e instanceof Error ? e : new Error(String(e)));
133
+ return false;
134
+ }
135
+ };
136
+
137
+ return {
138
+ isUploading,
139
+ error,
140
+ clearError,
141
+ upload,
142
+ download,
143
+ remove,
144
+ exists,
145
+ };
146
+ }
@@ -0,0 +1,44 @@
1
+ import type { Accessor } from 'solid-js';
2
+ import { useDb } from './context';
3
+ import { fromSubscription } from './from-subscription';
4
+ import type { StorageHealth, StorageHealthStatus } from '@spooky-sync/core';
5
+
6
+ export interface UseStorageStatus {
7
+ /** Full durability snapshot; updates reactively. */
8
+ health: Accessor<StorageHealth>;
9
+ /** `'unknown'` | `'persistent'` | `'memory'`. */
10
+ status: Accessor<StorageHealthStatus>;
11
+ /** `true` when the local store survives a reload. */
12
+ isPersistent: Accessor<boolean>;
13
+ /**
14
+ * `true` only when durable storage was requested and could NOT be opened, so
15
+ * the dataset is sitting in RAM and local writes die on reload. Drive a
16
+ * warning off this, not off `status`: a store configured as in-memory reports
17
+ * `'memory'` too, and that is a choice rather than a problem.
18
+ */
19
+ isMemoryFallback: Accessor<boolean>;
20
+ }
21
+
22
+ /**
23
+ * Observe how durable the LOCAL cache is, for a "no local storage" warning.
24
+ *
25
+ * Under `localEngine: 'sqlite'` with `store: 'indexeddb'` the durable store is
26
+ * the OPFS SAHPool VFS, and only ONE client per bucket can hold it open: a
27
+ * second tab of the same app cannot get it and runs in memory instead (the
28
+ * engine retries first, so a closing tab's lock is usually waited out). Must be
29
+ * used within a `<Sp00kyProvider>`.
30
+ */
31
+ export function useStorageStatus(): UseStorageStatus {
32
+ const db = useDb();
33
+ const health = fromSubscription<StorageHealth>(
34
+ (cb) => db.subscribeToStorageHealth(cb),
35
+ db.storageHealth
36
+ );
37
+
38
+ return {
39
+ health,
40
+ status: () => health().status,
41
+ isPersistent: () => health().status === 'persistent',
42
+ isMemoryFallback: () => health().fallback,
43
+ };
44
+ }
@@ -0,0 +1,63 @@
1
+ import type { Accessor } from 'solid-js';
2
+ import { useDb } from './context';
3
+ import { fromSubscription } from './from-subscription';
4
+ import type { ConnectionState, SyncHealth, SyncHealthStatus } from '@spooky-sync/core';
5
+
6
+ export interface UseSyncStatus {
7
+ /** Full health snapshot; updates reactively on every transition. */
8
+ health: Accessor<SyncHealth>;
9
+ /** `'healthy'` | `'degraded'`. */
10
+ status: Accessor<SyncHealthStatus>;
11
+ isHealthy: Accessor<boolean>;
12
+ /** `true` once sync has failed for a sustained run — drive a banner off this. */
13
+ isDegraded: Accessor<boolean>;
14
+ /** `true` once at least one sync round has succeeded this session. */
15
+ everConnected: Accessor<boolean>;
16
+ /**
17
+ * `true` only for a real lost connection: degraded AFTER a first successful
18
+ * sync. Stays `false` during the initial "connecting" phase (degraded but
19
+ * never reached the server yet), so an indicator can show nothing until the
20
+ * app has actually connected once.
21
+ */
22
+ isOffline: Accessor<boolean>;
23
+ /**
24
+ * Transport state of the remote WebSocket. Flips the instant the socket
25
+ * drops, unlike `status`, which only degrades after a sustained run of failed
26
+ * sync rounds — so this is what to drive a "reconnecting…" affordance off.
27
+ */
28
+ connection: Accessor<ConnectionState>;
29
+ /**
30
+ * `true` while the connection is being re-established. Usually still
31
+ * `isHealthy()`: a short reconnect is invisible to sync, and writes made
32
+ * during it are queued locally and pushed once the socket is back.
33
+ */
34
+ isReconnecting: Accessor<boolean>;
35
+ }
36
+
37
+ /**
38
+ * Observe sync health for a "can't reach the server" banner / indicator.
39
+ *
40
+ * Backed by `db.subscribeToSyncHealth`. Individual sync failures (a transient
41
+ * remote 500 on query registration, a dropped socket) are absorbed by the
42
+ * retry and never flip this; `isDegraded()` only goes true once failures
43
+ * persist for the configured number of consecutive rounds (sp00ky core config
44
+ * `syncHealth.degradeAfterConsecutiveFailures`, default 3), and flips back on
45
+ * the next successful round. Must be used within a `<Sp00kyProvider>`.
46
+ */
47
+ export function useSyncStatus(): UseSyncStatus {
48
+ const db = useDb();
49
+ // The subscription fires synchronously with the current status; the initial
50
+ // value (loadingValue) just avoids a flash before it lands.
51
+ const health = fromSubscription<SyncHealth>((cb) => db.subscribeToSyncHealth(cb), db.syncHealth);
52
+
53
+ return {
54
+ health,
55
+ status: () => health().status,
56
+ isHealthy: () => health().status === 'healthy',
57
+ isDegraded: () => health().status === 'degraded',
58
+ everConnected: () => health().everConnected,
59
+ isOffline: () => health().status === 'degraded' && health().everConnected,
60
+ connection: () => health().connection,
61
+ isReconnecting: () => health().connection === 'reconnecting',
62
+ };
63
+ }
@@ -0,0 +1,83 @@
1
+ import type { SyncedDb } from '../index';
2
+ import type { GenericSchema } from '../lib/models';
3
+ import type { Sp00kyConfig } from '@spooky-sync/core';
4
+ import type { SchemaStructure, TableNames, GetTable, TableModel } from '@spooky-sync/query-builder';
5
+
6
+ /**
7
+ * Options for database provisioning
8
+ */
9
+ export interface ProvisionOptions {
10
+ /** Force re-provision even if schema already exists */
11
+ force?: boolean;
12
+ }
13
+
14
+ declare global {
15
+ interface Window {
16
+ db?: SyncedDb<any>;
17
+ }
18
+ }
19
+
20
+ export type CacheStrategy = 'memory' | 'indexeddb';
21
+
22
+ /**
23
+ * Infer Schema type (Record<TableName, Model>) from schema const
24
+ */
25
+ export type InferSchemaFromConst<S extends SchemaStructure> = {
26
+ [K in TableNames<S>]: TableModel<GetTable<S, K>>;
27
+ };
28
+
29
+ /**
30
+ * Infer Relationships type from schema const's relationships array
31
+ * Converts from array format to nested object format
32
+ */
33
+ export type InferRelationshipsFromConst<S extends SchemaStructure, Schema extends GenericSchema> = {
34
+ [TableName in TableNames<S>]: {
35
+ [Rel in Extract<S['relationships'][number], { from: TableName }> as Rel['field']]: {
36
+ model: Rel['to'] extends keyof Schema ? Schema[Rel['to']] : any;
37
+ table: Rel['to'];
38
+ cardinality: Rel['cardinality'];
39
+ };
40
+ };
41
+ };
42
+
43
+ // Prettify helper expands types for better intellisense
44
+ type Prettify<T> = { [K in keyof T]: T[K] } & {};
45
+
46
+ export type SyncedDbConfig<S extends SchemaStructure> = Prettify<Sp00kyConfig<S>>;
47
+
48
+ // export interface LocalDbConfig {
49
+ // name: string;
50
+ // storageStrategy: CacheStrategy;
51
+ // namespace?: string;
52
+ // database?: string;
53
+ // }
54
+
55
+ // export interface RemoteDbConfig {
56
+ // url: string;
57
+ // token?: string;
58
+ // namespace?: string;
59
+ // database?: string;
60
+ // }
61
+
62
+ // export interface DbConnection {
63
+ // internal: Surreal;
64
+ // local: Surreal;
65
+ // remote?: Surreal;
66
+ // }
67
+
68
+ // export interface SyncStatus {
69
+ // isConnected: boolean;
70
+ // lastSync?: Date;
71
+ // pendingChanges: number;
72
+ // localRecords: number;
73
+ // remoteRecords?: number;
74
+ // }
75
+
76
+ // export interface SyncOptions {
77
+ // /** Force full sync regardless of last sync time */
78
+ // force?: boolean;
79
+ // /** Sync only specific tables */
80
+ // tables?: string[];
81
+ // /** Batch size for sync operations */
82
+ // batchSize?: number;
83
+ // }
package/tsconfig.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "skipLibCheck": true,
7
+ "esModuleInterop": true,
8
+ "allowSyntheticDefaultImports": true,
9
+ "strict": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "noEmit": true,
16
+ "declaration": true,
17
+ "declarationMap": true,
18
+ "emitDeclarationOnly": false,
19
+ "outDir": "./dist",
20
+ "paths": {
21
+ "@spooky-sync/query-builder": ["../query-builder/src/index.ts"],
22
+ "@spooky-sync/core": ["../core/src/index.ts"]
23
+ }
24
+ },
25
+ "include": ["src/**/*"],
26
+ "exclude": ["node_modules", "dist"]
27
+ }
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from 'tsdown';
2
+
3
+ export default defineConfig({
4
+ entry: ['src/index.ts'],
5
+ format: ['esm', 'cjs'],
6
+ dts: true,
7
+ external: [
8
+ 'surrealdb',
9
+ '@surrealdb/wasm',
10
+ 'solid-js',
11
+ '@spooky-sync/core',
12
+ '@spooky-sync/query-builder',
13
+ ],
14
+ clean: true,
15
+ hash: false,
16
+ sourcemap: true,
17
+ target: 'es2020',
18
+ });
@@ -0,0 +1,14 @@
1
+ import { defineConfig } from 'vitest/config';
2
+
3
+ export default defineConfig({
4
+ // solid-js's `node` export condition resolves the SSR build, where user
5
+ // effects intentionally never run. Force the browser dev build so tests
6
+ // exercise real client-side reactivity semantics.
7
+ resolve: {
8
+ conditions: ['browser', 'development'],
9
+ },
10
+ test: {
11
+ environment: 'node',
12
+ include: ['src/**/*.test.ts'],
13
+ },
14
+ });