@taladb/next 0.9.0
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/LICENSE +21 -0
- package/dist/client.d.mts +44 -0
- package/dist/client.d.ts +44 -0
- package/dist/client.js +105 -0
- package/dist/client.mjs +71 -0
- package/dist/server.d.mts +85 -0
- package/dist/server.d.ts +85 -0
- package/dist/server.js +168 -0
- package/dist/server.mjs +141 -0
- package/package.json +72 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 thinkgrid-labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { SyncOptions, SyncResult } from 'taladb';
|
|
4
|
+
|
|
5
|
+
interface SyncProviderProps {
|
|
6
|
+
/** Base URL of your sync backend, e.g. `/api/sync` (same-origin route
|
|
7
|
+
* handler created with `createSyncHandlers`) or a full URL. */
|
|
8
|
+
endpoint: string;
|
|
9
|
+
/** Milliseconds between passes while the tab is open. Default 30 000. Set
|
|
10
|
+
* `0` to disable the interval (event-driven passes still run). */
|
|
11
|
+
intervalMs?: number;
|
|
12
|
+
/** Headers for every request — typically `Authorization`. Pass a function
|
|
13
|
+
* to read a fresh token per pass. */
|
|
14
|
+
headers?: Record<string, string> | (() => Record<string, string>);
|
|
15
|
+
/** Scope/direction options forwarded to every `db.sync()` pass. */
|
|
16
|
+
options?: SyncOptions;
|
|
17
|
+
/** Called after each successful pass. */
|
|
18
|
+
onSync?: (result: SyncResult) => void;
|
|
19
|
+
/** Called when a pass fails (offline, 401, server down). The next replayed
|
|
20
|
+
* pass covers the gap. */
|
|
21
|
+
onError?: (error: unknown) => void;
|
|
22
|
+
children?: ReactNode;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* ```tsx
|
|
26
|
+
* // app/providers.tsx
|
|
27
|
+
* 'use client'
|
|
28
|
+
* import { TalaDBProvider } from '@taladb/react'
|
|
29
|
+
* import { SyncProvider } from '@taladb/next/client'
|
|
30
|
+
*
|
|
31
|
+
* export function Providers({ children }) {
|
|
32
|
+
* return (
|
|
33
|
+
* <TalaDBProvider name="myapp.db">
|
|
34
|
+
* <SyncProvider endpoint="/api/sync" headers={() => ({ Authorization: `Bearer ${getToken()}` })}>
|
|
35
|
+
* {children}
|
|
36
|
+
* </SyncProvider>
|
|
37
|
+
* </TalaDBProvider>
|
|
38
|
+
* )
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
declare function SyncProvider({ endpoint, intervalMs, headers, options, onSync, onError, children, }: SyncProviderProps): react_jsx_runtime.JSX.Element;
|
|
43
|
+
|
|
44
|
+
export { SyncProvider, type SyncProviderProps };
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
+
import { ReactNode } from 'react';
|
|
3
|
+
import { SyncOptions, SyncResult } from 'taladb';
|
|
4
|
+
|
|
5
|
+
interface SyncProviderProps {
|
|
6
|
+
/** Base URL of your sync backend, e.g. `/api/sync` (same-origin route
|
|
7
|
+
* handler created with `createSyncHandlers`) or a full URL. */
|
|
8
|
+
endpoint: string;
|
|
9
|
+
/** Milliseconds between passes while the tab is open. Default 30 000. Set
|
|
10
|
+
* `0` to disable the interval (event-driven passes still run). */
|
|
11
|
+
intervalMs?: number;
|
|
12
|
+
/** Headers for every request — typically `Authorization`. Pass a function
|
|
13
|
+
* to read a fresh token per pass. */
|
|
14
|
+
headers?: Record<string, string> | (() => Record<string, string>);
|
|
15
|
+
/** Scope/direction options forwarded to every `db.sync()` pass. */
|
|
16
|
+
options?: SyncOptions;
|
|
17
|
+
/** Called after each successful pass. */
|
|
18
|
+
onSync?: (result: SyncResult) => void;
|
|
19
|
+
/** Called when a pass fails (offline, 401, server down). The next replayed
|
|
20
|
+
* pass covers the gap. */
|
|
21
|
+
onError?: (error: unknown) => void;
|
|
22
|
+
children?: ReactNode;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* ```tsx
|
|
26
|
+
* // app/providers.tsx
|
|
27
|
+
* 'use client'
|
|
28
|
+
* import { TalaDBProvider } from '@taladb/react'
|
|
29
|
+
* import { SyncProvider } from '@taladb/next/client'
|
|
30
|
+
*
|
|
31
|
+
* export function Providers({ children }) {
|
|
32
|
+
* return (
|
|
33
|
+
* <TalaDBProvider name="myapp.db">
|
|
34
|
+
* <SyncProvider endpoint="/api/sync" headers={() => ({ Authorization: `Bearer ${getToken()}` })}>
|
|
35
|
+
* {children}
|
|
36
|
+
* </SyncProvider>
|
|
37
|
+
* </TalaDBProvider>
|
|
38
|
+
* )
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
declare function SyncProvider({ endpoint, intervalMs, headers, options, onSync, onError, children, }: SyncProviderProps): react_jsx_runtime.JSX.Element;
|
|
43
|
+
|
|
44
|
+
export { SyncProvider, type SyncProviderProps };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var __copyProps = (to, from, except, desc) => {
|
|
14
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
15
|
+
for (let key of __getOwnPropNames(from))
|
|
16
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
17
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
18
|
+
}
|
|
19
|
+
return to;
|
|
20
|
+
};
|
|
21
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
22
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
23
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
24
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
25
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
26
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
27
|
+
mod
|
|
28
|
+
));
|
|
29
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
30
|
+
|
|
31
|
+
// src/client.tsx
|
|
32
|
+
var client_exports = {};
|
|
33
|
+
__export(client_exports, {
|
|
34
|
+
SyncProvider: () => SyncProvider
|
|
35
|
+
});
|
|
36
|
+
module.exports = __toCommonJS(client_exports);
|
|
37
|
+
var import_react = require("react");
|
|
38
|
+
var import_react2 = require("@taladb/react");
|
|
39
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
40
|
+
function SyncProvider({
|
|
41
|
+
endpoint,
|
|
42
|
+
intervalMs = 3e4,
|
|
43
|
+
headers,
|
|
44
|
+
options,
|
|
45
|
+
onSync,
|
|
46
|
+
onError,
|
|
47
|
+
children
|
|
48
|
+
}) {
|
|
49
|
+
const db = (0, import_react2.useTalaDB)();
|
|
50
|
+
const latest = (0, import_react.useRef)({ headers, options, onSync, onError });
|
|
51
|
+
latest.current = { headers, options, onSync, onError };
|
|
52
|
+
const activePass = (0, import_react.useRef)(null);
|
|
53
|
+
(0, import_react.useEffect)(() => {
|
|
54
|
+
let stopped = false;
|
|
55
|
+
let waitingForActive = false;
|
|
56
|
+
const sync = async () => {
|
|
57
|
+
if (stopped) return;
|
|
58
|
+
if (activePass.current) {
|
|
59
|
+
if (waitingForActive) return;
|
|
60
|
+
waitingForActive = true;
|
|
61
|
+
await activePass.current;
|
|
62
|
+
waitingForActive = false;
|
|
63
|
+
if (!stopped) void sync();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const pass = Promise.resolve().then(async () => {
|
|
67
|
+
try {
|
|
68
|
+
const { HttpSyncAdapter } = await import("taladb");
|
|
69
|
+
const h = latest.current.headers;
|
|
70
|
+
const adapter = new HttpSyncAdapter({
|
|
71
|
+
endpoint,
|
|
72
|
+
headers: typeof h === "function" ? h() : h
|
|
73
|
+
});
|
|
74
|
+
const result = await db.sync(adapter, latest.current.options ?? {});
|
|
75
|
+
if (!stopped) latest.current.onSync?.(result);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
if (!stopped) latest.current.onError?.(e);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
activePass.current = pass;
|
|
81
|
+
await pass.finally(() => {
|
|
82
|
+
if (activePass.current === pass) activePass.current = null;
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
void sync();
|
|
86
|
+
const tick = intervalMs > 0 ? setInterval(sync, intervalMs) : void 0;
|
|
87
|
+
const onOnline = () => void sync();
|
|
88
|
+
const onVisible = () => {
|
|
89
|
+
if (document.visibilityState === "visible") void sync();
|
|
90
|
+
};
|
|
91
|
+
window.addEventListener("online", onOnline);
|
|
92
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
93
|
+
return () => {
|
|
94
|
+
stopped = true;
|
|
95
|
+
if (tick !== void 0) clearInterval(tick);
|
|
96
|
+
window.removeEventListener("online", onOnline);
|
|
97
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
98
|
+
};
|
|
99
|
+
}, [db, endpoint, intervalMs]);
|
|
100
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children });
|
|
101
|
+
}
|
|
102
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
103
|
+
0 && (module.exports = {
|
|
104
|
+
SyncProvider
|
|
105
|
+
});
|
package/dist/client.mjs
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// src/client.tsx
|
|
4
|
+
import { useEffect, useRef } from "react";
|
|
5
|
+
import { useTalaDB } from "@taladb/react";
|
|
6
|
+
import { Fragment, jsx } from "react/jsx-runtime";
|
|
7
|
+
function SyncProvider({
|
|
8
|
+
endpoint,
|
|
9
|
+
intervalMs = 3e4,
|
|
10
|
+
headers,
|
|
11
|
+
options,
|
|
12
|
+
onSync,
|
|
13
|
+
onError,
|
|
14
|
+
children
|
|
15
|
+
}) {
|
|
16
|
+
const db = useTalaDB();
|
|
17
|
+
const latest = useRef({ headers, options, onSync, onError });
|
|
18
|
+
latest.current = { headers, options, onSync, onError };
|
|
19
|
+
const activePass = useRef(null);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
let stopped = false;
|
|
22
|
+
let waitingForActive = false;
|
|
23
|
+
const sync = async () => {
|
|
24
|
+
if (stopped) return;
|
|
25
|
+
if (activePass.current) {
|
|
26
|
+
if (waitingForActive) return;
|
|
27
|
+
waitingForActive = true;
|
|
28
|
+
await activePass.current;
|
|
29
|
+
waitingForActive = false;
|
|
30
|
+
if (!stopped) void sync();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const pass = Promise.resolve().then(async () => {
|
|
34
|
+
try {
|
|
35
|
+
const { HttpSyncAdapter } = await import("taladb");
|
|
36
|
+
const h = latest.current.headers;
|
|
37
|
+
const adapter = new HttpSyncAdapter({
|
|
38
|
+
endpoint,
|
|
39
|
+
headers: typeof h === "function" ? h() : h
|
|
40
|
+
});
|
|
41
|
+
const result = await db.sync(adapter, latest.current.options ?? {});
|
|
42
|
+
if (!stopped) latest.current.onSync?.(result);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
if (!stopped) latest.current.onError?.(e);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
activePass.current = pass;
|
|
48
|
+
await pass.finally(() => {
|
|
49
|
+
if (activePass.current === pass) activePass.current = null;
|
|
50
|
+
});
|
|
51
|
+
};
|
|
52
|
+
void sync();
|
|
53
|
+
const tick = intervalMs > 0 ? setInterval(sync, intervalMs) : void 0;
|
|
54
|
+
const onOnline = () => void sync();
|
|
55
|
+
const onVisible = () => {
|
|
56
|
+
if (document.visibilityState === "visible") void sync();
|
|
57
|
+
};
|
|
58
|
+
window.addEventListener("online", onOnline);
|
|
59
|
+
document.addEventListener("visibilitychange", onVisible);
|
|
60
|
+
return () => {
|
|
61
|
+
stopped = true;
|
|
62
|
+
if (tick !== void 0) clearInterval(tick);
|
|
63
|
+
window.removeEventListener("online", onOnline);
|
|
64
|
+
document.removeEventListener("visibilitychange", onVisible);
|
|
65
|
+
};
|
|
66
|
+
}, [db, endpoint, intervalMs]);
|
|
67
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
68
|
+
}
|
|
69
|
+
export {
|
|
70
|
+
SyncProvider
|
|
71
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { TalaDB } from 'taladb';
|
|
2
|
+
|
|
3
|
+
/** The fields of a TalaDB change record the store keys on. Everything else in
|
|
4
|
+
* a record is opaque — store it verbatim. */
|
|
5
|
+
interface ChangeRecord {
|
|
6
|
+
collection: string;
|
|
7
|
+
id: string;
|
|
8
|
+
changed_at: number;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Where pushed changes live. Implementations keep the **latest change per
|
|
13
|
+
* document per scope** (Last-Write-Wins by `changed_at`) and answer
|
|
14
|
+
* "everything in this scope newer than `sinceMs`".
|
|
15
|
+
*
|
|
16
|
+
* `scope` partitions users or workspaces — the value returned by
|
|
17
|
+
* `authorize`. A store must never leak changes across scopes.
|
|
18
|
+
*/
|
|
19
|
+
interface SyncStore {
|
|
20
|
+
/** Merge a pushed changeset (JSON string of `ChangeRecord[]`) into `scope`. */
|
|
21
|
+
push(changeset: string, scope: string): Promise<void>;
|
|
22
|
+
/** Serialized changeset of `scope` changes with `changed_at > sinceMs`. */
|
|
23
|
+
pull(sinceMs: number, scope: string): Promise<string>;
|
|
24
|
+
}
|
|
25
|
+
interface CreateSyncHandlersOptions {
|
|
26
|
+
store: SyncStore;
|
|
27
|
+
/** Maximum accepted push body size in bytes. Default 1 MiB. */
|
|
28
|
+
maxBodyBytes?: number;
|
|
29
|
+
/** Maximum records accepted in one push. Default 10,000. */
|
|
30
|
+
maxRecords?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Identify and authorize the caller, returning a scope key (e.g. the user
|
|
33
|
+
* id) — this is your security boundary. Return `null`/`undefined` to reject
|
|
34
|
+
* with 401. Omit for a single shared scope (`'default'`) — fine for
|
|
35
|
+
* prototypes and single-user apps, wrong for anything multi-user.
|
|
36
|
+
*/
|
|
37
|
+
authorize?: (req: Request) => Promise<string | null | undefined> | string | null | undefined;
|
|
38
|
+
}
|
|
39
|
+
interface SyncHandlers {
|
|
40
|
+
/** Mount as the handler for `POST {endpoint}/push`. */
|
|
41
|
+
POST(req: Request): Promise<Response>;
|
|
42
|
+
/** Mount as the handler for `GET {endpoint}/pull`. */
|
|
43
|
+
GET(req: Request): Promise<Response>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A complete sync backend as a pair of fetch-style route handlers.
|
|
47
|
+
*
|
|
48
|
+
* ```ts
|
|
49
|
+
* // app/api/sync/[[...action]]/route.ts
|
|
50
|
+
* import { createSyncHandlers, memorySyncStore } from '@taladb/next/server'
|
|
51
|
+
*
|
|
52
|
+
* export const { POST, GET } = createSyncHandlers({
|
|
53
|
+
* store: memorySyncStore(),
|
|
54
|
+
* authorize: async (req) => verifySession(req.headers.get('authorization')),
|
|
55
|
+
* })
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* Point the client's `HttpSyncAdapter` at `endpoint: '/api/sync'`.
|
|
59
|
+
*/
|
|
60
|
+
declare function createSyncHandlers(options: CreateSyncHandlersOptions): SyncHandlers;
|
|
61
|
+
/**
|
|
62
|
+
* In-memory store — perfect for development and tests; state is lost on
|
|
63
|
+
* server restart and not shared across serverless instances. Use
|
|
64
|
+
* {@link taladbSyncStore} (or your own database) in production.
|
|
65
|
+
*/
|
|
66
|
+
declare function memorySyncStore(): SyncStore;
|
|
67
|
+
/**
|
|
68
|
+
* A store backed by a server-side TalaDB — TalaDB syncing to TalaDB. Open a
|
|
69
|
+
* file-backed database once (Node.js runtime, not edge) and hand it in:
|
|
70
|
+
*
|
|
71
|
+
* ```ts
|
|
72
|
+
* import { openDB } from 'taladb'
|
|
73
|
+
* import { createSyncHandlers, taladbSyncStore } from '@taladb/next/server'
|
|
74
|
+
*
|
|
75
|
+
* const serverDb = await openDB('sync-hub.db')
|
|
76
|
+
* export const { POST, GET } = createSyncHandlers({ store: taladbSyncStore(serverDb) })
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* Stores the greatest timestamp per synced document and preserves distinct
|
|
80
|
+
* equal-timestamp candidates so TalaDB core can apply its exact deterministic
|
|
81
|
+
* tie-break when clients import them.
|
|
82
|
+
*/
|
|
83
|
+
declare function taladbSyncStore(db: TalaDB, collectionName?: string): SyncStore;
|
|
84
|
+
|
|
85
|
+
export { type ChangeRecord, type CreateSyncHandlersOptions, type SyncHandlers, type SyncStore, createSyncHandlers, memorySyncStore, taladbSyncStore };
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { TalaDB } from 'taladb';
|
|
2
|
+
|
|
3
|
+
/** The fields of a TalaDB change record the store keys on. Everything else in
|
|
4
|
+
* a record is opaque — store it verbatim. */
|
|
5
|
+
interface ChangeRecord {
|
|
6
|
+
collection: string;
|
|
7
|
+
id: string;
|
|
8
|
+
changed_at: number;
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Where pushed changes live. Implementations keep the **latest change per
|
|
13
|
+
* document per scope** (Last-Write-Wins by `changed_at`) and answer
|
|
14
|
+
* "everything in this scope newer than `sinceMs`".
|
|
15
|
+
*
|
|
16
|
+
* `scope` partitions users or workspaces — the value returned by
|
|
17
|
+
* `authorize`. A store must never leak changes across scopes.
|
|
18
|
+
*/
|
|
19
|
+
interface SyncStore {
|
|
20
|
+
/** Merge a pushed changeset (JSON string of `ChangeRecord[]`) into `scope`. */
|
|
21
|
+
push(changeset: string, scope: string): Promise<void>;
|
|
22
|
+
/** Serialized changeset of `scope` changes with `changed_at > sinceMs`. */
|
|
23
|
+
pull(sinceMs: number, scope: string): Promise<string>;
|
|
24
|
+
}
|
|
25
|
+
interface CreateSyncHandlersOptions {
|
|
26
|
+
store: SyncStore;
|
|
27
|
+
/** Maximum accepted push body size in bytes. Default 1 MiB. */
|
|
28
|
+
maxBodyBytes?: number;
|
|
29
|
+
/** Maximum records accepted in one push. Default 10,000. */
|
|
30
|
+
maxRecords?: number;
|
|
31
|
+
/**
|
|
32
|
+
* Identify and authorize the caller, returning a scope key (e.g. the user
|
|
33
|
+
* id) — this is your security boundary. Return `null`/`undefined` to reject
|
|
34
|
+
* with 401. Omit for a single shared scope (`'default'`) — fine for
|
|
35
|
+
* prototypes and single-user apps, wrong for anything multi-user.
|
|
36
|
+
*/
|
|
37
|
+
authorize?: (req: Request) => Promise<string | null | undefined> | string | null | undefined;
|
|
38
|
+
}
|
|
39
|
+
interface SyncHandlers {
|
|
40
|
+
/** Mount as the handler for `POST {endpoint}/push`. */
|
|
41
|
+
POST(req: Request): Promise<Response>;
|
|
42
|
+
/** Mount as the handler for `GET {endpoint}/pull`. */
|
|
43
|
+
GET(req: Request): Promise<Response>;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A complete sync backend as a pair of fetch-style route handlers.
|
|
47
|
+
*
|
|
48
|
+
* ```ts
|
|
49
|
+
* // app/api/sync/[[...action]]/route.ts
|
|
50
|
+
* import { createSyncHandlers, memorySyncStore } from '@taladb/next/server'
|
|
51
|
+
*
|
|
52
|
+
* export const { POST, GET } = createSyncHandlers({
|
|
53
|
+
* store: memorySyncStore(),
|
|
54
|
+
* authorize: async (req) => verifySession(req.headers.get('authorization')),
|
|
55
|
+
* })
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* Point the client's `HttpSyncAdapter` at `endpoint: '/api/sync'`.
|
|
59
|
+
*/
|
|
60
|
+
declare function createSyncHandlers(options: CreateSyncHandlersOptions): SyncHandlers;
|
|
61
|
+
/**
|
|
62
|
+
* In-memory store — perfect for development and tests; state is lost on
|
|
63
|
+
* server restart and not shared across serverless instances. Use
|
|
64
|
+
* {@link taladbSyncStore} (or your own database) in production.
|
|
65
|
+
*/
|
|
66
|
+
declare function memorySyncStore(): SyncStore;
|
|
67
|
+
/**
|
|
68
|
+
* A store backed by a server-side TalaDB — TalaDB syncing to TalaDB. Open a
|
|
69
|
+
* file-backed database once (Node.js runtime, not edge) and hand it in:
|
|
70
|
+
*
|
|
71
|
+
* ```ts
|
|
72
|
+
* import { openDB } from 'taladb'
|
|
73
|
+
* import { createSyncHandlers, taladbSyncStore } from '@taladb/next/server'
|
|
74
|
+
*
|
|
75
|
+
* const serverDb = await openDB('sync-hub.db')
|
|
76
|
+
* export const { POST, GET } = createSyncHandlers({ store: taladbSyncStore(serverDb) })
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* Stores the greatest timestamp per synced document and preserves distinct
|
|
80
|
+
* equal-timestamp candidates so TalaDB core can apply its exact deterministic
|
|
81
|
+
* tie-break when clients import them.
|
|
82
|
+
*/
|
|
83
|
+
declare function taladbSyncStore(db: TalaDB, collectionName?: string): SyncStore;
|
|
84
|
+
|
|
85
|
+
export { type ChangeRecord, type CreateSyncHandlersOptions, type SyncHandlers, type SyncStore, createSyncHandlers, memorySyncStore, taladbSyncStore };
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/server.ts
|
|
21
|
+
var server_exports = {};
|
|
22
|
+
__export(server_exports, {
|
|
23
|
+
createSyncHandlers: () => createSyncHandlers,
|
|
24
|
+
memorySyncStore: () => memorySyncStore,
|
|
25
|
+
taladbSyncStore: () => taladbSyncStore
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(server_exports);
|
|
28
|
+
function createSyncHandlers(options) {
|
|
29
|
+
const { store } = options;
|
|
30
|
+
const maxBodyBytes = options.maxBodyBytes ?? 1024 * 1024;
|
|
31
|
+
const maxRecords = options.maxRecords ?? 1e4;
|
|
32
|
+
const authorize = options.authorize ?? (() => "default");
|
|
33
|
+
async function resolveScope(req) {
|
|
34
|
+
const scope = await authorize(req);
|
|
35
|
+
return scope ?? null;
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
async POST(req) {
|
|
39
|
+
const scope = await resolveScope(req);
|
|
40
|
+
if (scope === null) return new Response("unauthorized", { status: 401 });
|
|
41
|
+
const declaredLength = Number(req.headers.get("content-length"));
|
|
42
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxBodyBytes) {
|
|
43
|
+
return new Response("changeset too large", { status: 413 });
|
|
44
|
+
}
|
|
45
|
+
const body = await req.text();
|
|
46
|
+
if (new TextEncoder().encode(body).byteLength > maxBodyBytes) {
|
|
47
|
+
return new Response("changeset too large", { status: 413 });
|
|
48
|
+
}
|
|
49
|
+
let records;
|
|
50
|
+
try {
|
|
51
|
+
records = JSON.parse(body === "" ? "[]" : body);
|
|
52
|
+
} catch {
|
|
53
|
+
return new Response("invalid changeset: not JSON", { status: 400 });
|
|
54
|
+
}
|
|
55
|
+
if (!Array.isArray(records)) {
|
|
56
|
+
return new Response("invalid changeset: expected a JSON array", { status: 400 });
|
|
57
|
+
}
|
|
58
|
+
if (records.length > maxRecords) {
|
|
59
|
+
return new Response("too many change records", { status: 413 });
|
|
60
|
+
}
|
|
61
|
+
for (const r of records) {
|
|
62
|
+
const rec = r;
|
|
63
|
+
if (typeof rec !== "object" || rec === null || typeof rec.collection !== "string" || typeof rec.id !== "string" || typeof rec.changed_at !== "number" || !Number.isSafeInteger(rec.changed_at) || rec.changed_at < 0) {
|
|
64
|
+
return new Response("invalid changeset: malformed change record", { status: 400 });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (records.length > 0) {
|
|
68
|
+
await store.push(JSON.stringify(records), scope);
|
|
69
|
+
}
|
|
70
|
+
return new Response(null, { status: 204 });
|
|
71
|
+
},
|
|
72
|
+
async GET(req) {
|
|
73
|
+
const scope = await resolveScope(req);
|
|
74
|
+
if (scope === null) return new Response("unauthorized", { status: 401 });
|
|
75
|
+
const sinceRaw = new URL(req.url).searchParams.get("since") ?? "0";
|
|
76
|
+
const since = Number(sinceRaw);
|
|
77
|
+
if (!Number.isSafeInteger(since) || since < 0) {
|
|
78
|
+
return new Response("invalid since parameter", { status: 400 });
|
|
79
|
+
}
|
|
80
|
+
const changeset = await store.pull(since, scope);
|
|
81
|
+
return new Response(changeset, {
|
|
82
|
+
status: 200,
|
|
83
|
+
headers: { "content-type": "application/json" }
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function memorySyncStore() {
|
|
89
|
+
const scopes = /* @__PURE__ */ new Map();
|
|
90
|
+
return {
|
|
91
|
+
async push(changeset, scope) {
|
|
92
|
+
let docs = scopes.get(scope);
|
|
93
|
+
if (!docs) {
|
|
94
|
+
docs = /* @__PURE__ */ new Map();
|
|
95
|
+
scopes.set(scope, docs);
|
|
96
|
+
}
|
|
97
|
+
for (const change of JSON.parse(changeset)) {
|
|
98
|
+
const key = `${change.collection}::${change.id}`;
|
|
99
|
+
const existing = docs.get(key) ?? [];
|
|
100
|
+
const latest = existing[0]?.changed_at;
|
|
101
|
+
if (latest === void 0 || change.changed_at > latest) docs.set(key, [change]);
|
|
102
|
+
else if (change.changed_at === latest) {
|
|
103
|
+
const serialized = JSON.stringify(change);
|
|
104
|
+
if (!existing.some((candidate) => JSON.stringify(candidate) === serialized)) {
|
|
105
|
+
existing.push(change);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
},
|
|
110
|
+
async pull(sinceMs, scope) {
|
|
111
|
+
const docs = scopes.get(scope);
|
|
112
|
+
if (!docs) return "[]";
|
|
113
|
+
return JSON.stringify(
|
|
114
|
+
[...docs.values()].flat().filter((c) => c.changed_at > sinceMs)
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
var taladbStoreQueues = /* @__PURE__ */ new WeakMap();
|
|
120
|
+
function taladbSyncStore(db, collectionName = "sync_changes") {
|
|
121
|
+
const col = db.collection(collectionName);
|
|
122
|
+
const indexed = (async () => {
|
|
123
|
+
await col.createIndex("doc_key").catch(() => {
|
|
124
|
+
});
|
|
125
|
+
await col.createIndex("changed_at").catch(() => {
|
|
126
|
+
});
|
|
127
|
+
})();
|
|
128
|
+
let queues = taladbStoreQueues.get(db);
|
|
129
|
+
if (!queues) {
|
|
130
|
+
queues = /* @__PURE__ */ new Map();
|
|
131
|
+
taladbStoreQueues.set(db, queues);
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
async push(changeset, scope) {
|
|
135
|
+
const run = async () => {
|
|
136
|
+
await indexed;
|
|
137
|
+
for (const change of JSON.parse(changeset)) {
|
|
138
|
+
const docKey = `${change.collection}::${change.id}`;
|
|
139
|
+
const serialized = JSON.stringify(change);
|
|
140
|
+
const existing = await col.find({ scope, doc_key: docKey });
|
|
141
|
+
const latest = existing.reduce((n, row) => Math.max(n, row.changed_at), -Infinity);
|
|
142
|
+
if (change.changed_at > latest) {
|
|
143
|
+
if (existing.length > 0) await col.deleteMany({ scope, doc_key: docKey });
|
|
144
|
+
await col.insert({ scope, doc_key: docKey, changed_at: change.changed_at, change: serialized });
|
|
145
|
+
} else if (change.changed_at === latest && !existing.some((row) => row.change === serialized)) {
|
|
146
|
+
await col.insert({ scope, doc_key: docKey, changed_at: change.changed_at, change: serialized });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const previous = queues.get(collectionName) ?? Promise.resolve();
|
|
151
|
+
const operation = previous.then(run, run);
|
|
152
|
+
queues.set(collectionName, operation.catch(() => {
|
|
153
|
+
}));
|
|
154
|
+
await operation;
|
|
155
|
+
},
|
|
156
|
+
async pull(sinceMs, scope) {
|
|
157
|
+
await indexed;
|
|
158
|
+
const rows = await col.find({ scope, changed_at: { $gt: sinceMs } });
|
|
159
|
+
return `[${rows.map((r) => r.change).join(",")}]`;
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
164
|
+
0 && (module.exports = {
|
|
165
|
+
createSyncHandlers,
|
|
166
|
+
memorySyncStore,
|
|
167
|
+
taladbSyncStore
|
|
168
|
+
});
|
package/dist/server.mjs
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// src/server.ts
|
|
2
|
+
function createSyncHandlers(options) {
|
|
3
|
+
const { store } = options;
|
|
4
|
+
const maxBodyBytes = options.maxBodyBytes ?? 1024 * 1024;
|
|
5
|
+
const maxRecords = options.maxRecords ?? 1e4;
|
|
6
|
+
const authorize = options.authorize ?? (() => "default");
|
|
7
|
+
async function resolveScope(req) {
|
|
8
|
+
const scope = await authorize(req);
|
|
9
|
+
return scope ?? null;
|
|
10
|
+
}
|
|
11
|
+
return {
|
|
12
|
+
async POST(req) {
|
|
13
|
+
const scope = await resolveScope(req);
|
|
14
|
+
if (scope === null) return new Response("unauthorized", { status: 401 });
|
|
15
|
+
const declaredLength = Number(req.headers.get("content-length"));
|
|
16
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxBodyBytes) {
|
|
17
|
+
return new Response("changeset too large", { status: 413 });
|
|
18
|
+
}
|
|
19
|
+
const body = await req.text();
|
|
20
|
+
if (new TextEncoder().encode(body).byteLength > maxBodyBytes) {
|
|
21
|
+
return new Response("changeset too large", { status: 413 });
|
|
22
|
+
}
|
|
23
|
+
let records;
|
|
24
|
+
try {
|
|
25
|
+
records = JSON.parse(body === "" ? "[]" : body);
|
|
26
|
+
} catch {
|
|
27
|
+
return new Response("invalid changeset: not JSON", { status: 400 });
|
|
28
|
+
}
|
|
29
|
+
if (!Array.isArray(records)) {
|
|
30
|
+
return new Response("invalid changeset: expected a JSON array", { status: 400 });
|
|
31
|
+
}
|
|
32
|
+
if (records.length > maxRecords) {
|
|
33
|
+
return new Response("too many change records", { status: 413 });
|
|
34
|
+
}
|
|
35
|
+
for (const r of records) {
|
|
36
|
+
const rec = r;
|
|
37
|
+
if (typeof rec !== "object" || rec === null || typeof rec.collection !== "string" || typeof rec.id !== "string" || typeof rec.changed_at !== "number" || !Number.isSafeInteger(rec.changed_at) || rec.changed_at < 0) {
|
|
38
|
+
return new Response("invalid changeset: malformed change record", { status: 400 });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (records.length > 0) {
|
|
42
|
+
await store.push(JSON.stringify(records), scope);
|
|
43
|
+
}
|
|
44
|
+
return new Response(null, { status: 204 });
|
|
45
|
+
},
|
|
46
|
+
async GET(req) {
|
|
47
|
+
const scope = await resolveScope(req);
|
|
48
|
+
if (scope === null) return new Response("unauthorized", { status: 401 });
|
|
49
|
+
const sinceRaw = new URL(req.url).searchParams.get("since") ?? "0";
|
|
50
|
+
const since = Number(sinceRaw);
|
|
51
|
+
if (!Number.isSafeInteger(since) || since < 0) {
|
|
52
|
+
return new Response("invalid since parameter", { status: 400 });
|
|
53
|
+
}
|
|
54
|
+
const changeset = await store.pull(since, scope);
|
|
55
|
+
return new Response(changeset, {
|
|
56
|
+
status: 200,
|
|
57
|
+
headers: { "content-type": "application/json" }
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function memorySyncStore() {
|
|
63
|
+
const scopes = /* @__PURE__ */ new Map();
|
|
64
|
+
return {
|
|
65
|
+
async push(changeset, scope) {
|
|
66
|
+
let docs = scopes.get(scope);
|
|
67
|
+
if (!docs) {
|
|
68
|
+
docs = /* @__PURE__ */ new Map();
|
|
69
|
+
scopes.set(scope, docs);
|
|
70
|
+
}
|
|
71
|
+
for (const change of JSON.parse(changeset)) {
|
|
72
|
+
const key = `${change.collection}::${change.id}`;
|
|
73
|
+
const existing = docs.get(key) ?? [];
|
|
74
|
+
const latest = existing[0]?.changed_at;
|
|
75
|
+
if (latest === void 0 || change.changed_at > latest) docs.set(key, [change]);
|
|
76
|
+
else if (change.changed_at === latest) {
|
|
77
|
+
const serialized = JSON.stringify(change);
|
|
78
|
+
if (!existing.some((candidate) => JSON.stringify(candidate) === serialized)) {
|
|
79
|
+
existing.push(change);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
async pull(sinceMs, scope) {
|
|
85
|
+
const docs = scopes.get(scope);
|
|
86
|
+
if (!docs) return "[]";
|
|
87
|
+
return JSON.stringify(
|
|
88
|
+
[...docs.values()].flat().filter((c) => c.changed_at > sinceMs)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
var taladbStoreQueues = /* @__PURE__ */ new WeakMap();
|
|
94
|
+
function taladbSyncStore(db, collectionName = "sync_changes") {
|
|
95
|
+
const col = db.collection(collectionName);
|
|
96
|
+
const indexed = (async () => {
|
|
97
|
+
await col.createIndex("doc_key").catch(() => {
|
|
98
|
+
});
|
|
99
|
+
await col.createIndex("changed_at").catch(() => {
|
|
100
|
+
});
|
|
101
|
+
})();
|
|
102
|
+
let queues = taladbStoreQueues.get(db);
|
|
103
|
+
if (!queues) {
|
|
104
|
+
queues = /* @__PURE__ */ new Map();
|
|
105
|
+
taladbStoreQueues.set(db, queues);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
async push(changeset, scope) {
|
|
109
|
+
const run = async () => {
|
|
110
|
+
await indexed;
|
|
111
|
+
for (const change of JSON.parse(changeset)) {
|
|
112
|
+
const docKey = `${change.collection}::${change.id}`;
|
|
113
|
+
const serialized = JSON.stringify(change);
|
|
114
|
+
const existing = await col.find({ scope, doc_key: docKey });
|
|
115
|
+
const latest = existing.reduce((n, row) => Math.max(n, row.changed_at), -Infinity);
|
|
116
|
+
if (change.changed_at > latest) {
|
|
117
|
+
if (existing.length > 0) await col.deleteMany({ scope, doc_key: docKey });
|
|
118
|
+
await col.insert({ scope, doc_key: docKey, changed_at: change.changed_at, change: serialized });
|
|
119
|
+
} else if (change.changed_at === latest && !existing.some((row) => row.change === serialized)) {
|
|
120
|
+
await col.insert({ scope, doc_key: docKey, changed_at: change.changed_at, change: serialized });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const previous = queues.get(collectionName) ?? Promise.resolve();
|
|
125
|
+
const operation = previous.then(run, run);
|
|
126
|
+
queues.set(collectionName, operation.catch(() => {
|
|
127
|
+
}));
|
|
128
|
+
await operation;
|
|
129
|
+
},
|
|
130
|
+
async pull(sinceMs, scope) {
|
|
131
|
+
await indexed;
|
|
132
|
+
const rows = await col.find({ scope, changed_at: { $gt: sinceMs } });
|
|
133
|
+
return `[${rows.map((r) => r.change).join(",")}]`;
|
|
134
|
+
}
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
export {
|
|
138
|
+
createSyncHandlers,
|
|
139
|
+
memorySyncStore,
|
|
140
|
+
taladbSyncStore
|
|
141
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@taladb/next",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "First-party Next.js integration for TalaDB — sync route handlers for the server, a SyncProvider for the client",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"exports": {
|
|
7
|
+
"./server": {
|
|
8
|
+
"types": "./dist/server.d.ts",
|
|
9
|
+
"import": "./dist/server.mjs",
|
|
10
|
+
"require": "./dist/server.js"
|
|
11
|
+
},
|
|
12
|
+
"./client": {
|
|
13
|
+
"types": "./dist/client.d.ts",
|
|
14
|
+
"import": "./dist/client.mjs",
|
|
15
|
+
"require": "./dist/client.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"database",
|
|
23
|
+
"local-first",
|
|
24
|
+
"nextjs",
|
|
25
|
+
"sync",
|
|
26
|
+
"offline",
|
|
27
|
+
"taladb"
|
|
28
|
+
],
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/thinkgrid-labs/taladb.git",
|
|
33
|
+
"directory": "packages/integrations/next"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://taladb.dev/guide/bidirectional-sync",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/thinkgrid-labs/taladb/issues"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@taladb/react": "^0.9.0",
|
|
44
|
+
"react": ">=18.0.0",
|
|
45
|
+
"taladb": "^0.9.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependenciesMeta": {
|
|
48
|
+
"@taladb/react": {
|
|
49
|
+
"optional": true
|
|
50
|
+
},
|
|
51
|
+
"react": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"@types/node": "^22.0.0",
|
|
57
|
+
"@types/react": "^18.0.0",
|
|
58
|
+
"react": "^18.0.0",
|
|
59
|
+
"tsup": "^8.0.0",
|
|
60
|
+
"typescript": "^5.9.3",
|
|
61
|
+
"vitest": "^3.2.0",
|
|
62
|
+
"taladb": "0.9.0",
|
|
63
|
+
"@taladb/react": "0.9.0",
|
|
64
|
+
"@taladb/node": "0.9.0"
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"build": "tsup",
|
|
68
|
+
"typecheck": "tsc --noEmit",
|
|
69
|
+
"test": "vitest run",
|
|
70
|
+
"test:watch": "vitest"
|
|
71
|
+
}
|
|
72
|
+
}
|