@spooky-sync/client-solid2 0.0.1-alpha.1
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 +81 -0
- package/QUICK_START.md +129 -0
- package/README.md +19 -0
- package/dist/index.cjs +1290 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +670 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +670 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1266 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
- package/skills/sp00ky-solid2/SKILL.md +71 -0
- package/src/index.ts +410 -0
- package/src/lib/Blurhash.ts +64 -0
- package/src/lib/BucketImage.ts +170 -0
- package/src/lib/Sp00kyProvider.ts +104 -0
- package/src/lib/__tests__/conflate.test.ts +120 -0
- package/src/lib/__tests__/create-query.test.ts +397 -0
- package/src/lib/__tests__/rc-semantics.test.ts +393 -0
- package/src/lib/__tests__/use-sync-activity.test.ts +104 -0
- package/src/lib/conflate.ts +74 -0
- package/src/lib/context.ts +28 -0
- package/src/lib/create-preload.ts +116 -0
- package/src/lib/create-query.ts +388 -0
- package/src/lib/create-submission.ts +57 -0
- package/src/lib/from-subscription.ts +32 -0
- package/src/lib/merge-rows.test.ts +92 -0
- package/src/lib/merge-rows.ts +72 -0
- package/src/lib/models.ts +8 -0
- package/src/lib/use-app-release.ts +89 -0
- package/src/lib/use-blurhash.ts +82 -0
- package/src/lib/use-bucket-image.ts +108 -0
- package/src/lib/use-crdt-field.ts +57 -0
- package/src/lib/use-download-file.ts +181 -0
- package/src/lib/use-feature-flag.ts +43 -0
- package/src/lib/use-file-upload.ts +146 -0
- package/src/lib/use-storage-status.ts +44 -0
- package/src/lib/use-sync-activity.ts +99 -0
- package/src/lib/use-sync-status.ts +63 -0
- package/src/types/index.ts +83 -0
- package/tsconfig.json +27 -0
- package/tsdown.config.ts +20 -0
- package/vitest.config.ts +14 -0
|
@@ -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,99 @@
|
|
|
1
|
+
import { createEffect, createSignal, onCleanup, type Accessor } from 'solid-js';
|
|
2
|
+
import { useDb } from './context';
|
|
3
|
+
import { fromSubscription } from './from-subscription';
|
|
4
|
+
import type { SyncedDb } from '../index';
|
|
5
|
+
import type { SchemaStructure } from '@spooky-sync/query-builder';
|
|
6
|
+
|
|
7
|
+
export interface UseSyncActivityOptions {
|
|
8
|
+
/**
|
|
9
|
+
* How long queries must be fetching before `isDownloading()` turns on.
|
|
10
|
+
* Filters the sub-frame fetches a warm cache produces, so the indicator only
|
|
11
|
+
* shows work the user could notice. Default 200 ms.
|
|
12
|
+
*/
|
|
13
|
+
downloadDelayMs?: number;
|
|
14
|
+
/**
|
|
15
|
+
* `isUploading()` turns on once MORE than this many mutations are waiting in
|
|
16
|
+
* the outbox. Default 1: a single write acknowledged within a round trip is
|
|
17
|
+
* not worth an animation; a backlog is.
|
|
18
|
+
*/
|
|
19
|
+
uploadThreshold?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UseSyncActivity {
|
|
23
|
+
/** Queries inside a fetch cycle right now (core `fetchingQueryCount`). */
|
|
24
|
+
fetchingQueries: Accessor<number>;
|
|
25
|
+
/** Locally committed writes the server has not acknowledged yet. */
|
|
26
|
+
pendingMutations: Accessor<number>;
|
|
27
|
+
/** Fetching for longer than `downloadDelayMs`. Drives a "downloading" mark. */
|
|
28
|
+
isDownloading: Accessor<boolean>;
|
|
29
|
+
/** More than `uploadThreshold` writes queued. Drives an "uploading" mark. */
|
|
30
|
+
isUploading: Accessor<boolean>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The two directions of sync traffic, for an indicator in the app chrome.
|
|
35
|
+
*
|
|
36
|
+
* `fetchingQueries` is one subscription on the engine's aggregate fetch count,
|
|
37
|
+
* not one per query; `pendingMutations` is the outbox depth. `isDownloading`
|
|
38
|
+
* is the fetch count debounced ON by `downloadDelayMs` (and off at once), so a
|
|
39
|
+
* local-first page that answers from cache and confirms with the server in a
|
|
40
|
+
* few milliseconds never flickers. Must be used within a `<Sp00kyProvider>`, or
|
|
41
|
+
* pass the `SyncedDb` explicitly.
|
|
42
|
+
*/
|
|
43
|
+
export function useSyncActivity<S extends SchemaStructure = any>(
|
|
44
|
+
dbOrOptions?: SyncedDb<S> | UseSyncActivityOptions,
|
|
45
|
+
maybeOptions?: UseSyncActivityOptions
|
|
46
|
+
): UseSyncActivity {
|
|
47
|
+
const explicitDb =
|
|
48
|
+
dbOrOptions && typeof (dbOrOptions as SyncedDb<S>).subscribeToFetchActivity === 'function'
|
|
49
|
+
? (dbOrOptions as SyncedDb<S>)
|
|
50
|
+
: undefined;
|
|
51
|
+
const options = (explicitDb ? maybeOptions : (dbOrOptions as UseSyncActivityOptions)) ?? {};
|
|
52
|
+
const db = explicitDb ?? useDb<S>();
|
|
53
|
+
const downloadDelayMs = options.downloadDelayMs ?? 200;
|
|
54
|
+
const uploadThreshold = options.uploadThreshold ?? 1;
|
|
55
|
+
|
|
56
|
+
const fetchingQueries = fromSubscription<number>(
|
|
57
|
+
(cb) => db.subscribeToFetchActivity(cb),
|
|
58
|
+
db.fetchingQueryCount
|
|
59
|
+
);
|
|
60
|
+
const pendingMutations = fromSubscription<number>(
|
|
61
|
+
(cb) => db.subscribeToPendingMutations(cb),
|
|
62
|
+
db.pendingMutationCount
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// ON after the delay, OFF immediately. Written from a timer, hence ownedWrite.
|
|
66
|
+
const [isDownloading, setIsDownloading] = createSignal(false, { ownedWrite: true });
|
|
67
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
68
|
+
createEffect(
|
|
69
|
+
() => fetchingQueries() > 0,
|
|
70
|
+
(busy) => {
|
|
71
|
+
if (timer !== undefined) {
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
timer = undefined;
|
|
74
|
+
}
|
|
75
|
+
if (!busy) {
|
|
76
|
+
setIsDownloading(false);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
if (downloadDelayMs <= 0) {
|
|
80
|
+
setIsDownloading(true);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
timer = setTimeout(() => {
|
|
84
|
+
timer = undefined;
|
|
85
|
+
setIsDownloading(true);
|
|
86
|
+
}, downloadDelayMs);
|
|
87
|
+
}
|
|
88
|
+
);
|
|
89
|
+
onCleanup(() => {
|
|
90
|
+
if (timer !== undefined) clearTimeout(timer);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
fetchingQueries,
|
|
95
|
+
pendingMutations,
|
|
96
|
+
isDownloading,
|
|
97
|
+
isUploading: () => pendingMutations() > uploadThreshold,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -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
|
+
}
|
package/tsdown.config.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
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
|
+
'@solidjs/signals',
|
|
12
|
+
'@solidjs/web',
|
|
13
|
+
'@spooky-sync/core',
|
|
14
|
+
'@spooky-sync/query-builder',
|
|
15
|
+
],
|
|
16
|
+
clean: true,
|
|
17
|
+
hash: false,
|
|
18
|
+
sourcemap: true,
|
|
19
|
+
target: 'es2020',
|
|
20
|
+
});
|
package/vitest.config.ts
ADDED
|
@@ -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
|
+
});
|