@hanzo/base 0.2.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/dist/chunk-5NHFZRMO.js +1011 -0
- package/dist/chunk-5NHFZRMO.js.map +1 -0
- package/dist/chunk-LBAV5X5P.js +996 -0
- package/dist/chunk-LBAV5X5P.js.map +1 -0
- package/dist/compat/index.d.ts +1 -0
- package/dist/compat/index.js +3 -0
- package/dist/compat/index.js.map +1 -0
- package/dist/core/index.d.ts +368 -0
- package/dist/core/index.js +3 -0
- package/dist/core/index.js.map +1 -0
- package/dist/crdt/index.d.ts +372 -0
- package/dist/crdt/index.js +3 -0
- package/dist/crdt/index.js.map +1 -0
- package/dist/react/index.d.ts +144 -0
- package/dist/react/index.js +283 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +71 -0
- package/src/compat/index.ts +24 -0
- package/src/core/client.ts +432 -0
- package/src/core/collection.ts +474 -0
- package/src/core/index.ts +37 -0
- package/src/core/realtime.ts +303 -0
- package/src/core/state.ts +112 -0
- package/src/core/store.ts +241 -0
- package/src/crdt/clock.ts +78 -0
- package/src/crdt/counter.ts +130 -0
- package/src/crdt/document.ts +194 -0
- package/src/crdt/index.ts +56 -0
- package/src/crdt/operations.ts +101 -0
- package/src/crdt/register.ts +106 -0
- package/src/crdt/set.ts +172 -0
- package/src/crdt/sync.ts +412 -0
- package/src/crdt/text.ts +274 -0
- package/src/react/context.tsx +87 -0
- package/src/react/hooks.ts +489 -0
- package/src/react/index.ts +56 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { BaseClient } from '../chunk-LBAV5X5P.js';
|
|
2
|
+
export { BaseClient, BaseClientError } from '../chunk-LBAV5X5P.js';
|
|
3
|
+
import { CRDTDocument, CRDTSync } from '../chunk-5NHFZRMO.js';
|
|
4
|
+
import { createContext, useRef, useEffect, useContext, useState, useCallback } from 'react';
|
|
5
|
+
import { jsx } from 'react/jsx-runtime';
|
|
6
|
+
|
|
7
|
+
var BaseContext = createContext(null);
|
|
8
|
+
function BaseProvider({ url, authStore, client: externalClient, children }) {
|
|
9
|
+
const clientRef = useRef(null);
|
|
10
|
+
if (externalClient) {
|
|
11
|
+
clientRef.current = externalClient;
|
|
12
|
+
} else if (!clientRef.current && url) {
|
|
13
|
+
const config = { url };
|
|
14
|
+
if (authStore) config.authStore = authStore;
|
|
15
|
+
clientRef.current = new BaseClient(config);
|
|
16
|
+
}
|
|
17
|
+
const client = clientRef.current;
|
|
18
|
+
if (!client) {
|
|
19
|
+
throw new Error("BaseProvider: provide either `url` or `client` prop");
|
|
20
|
+
}
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
return () => {
|
|
23
|
+
if (!externalClient) {
|
|
24
|
+
client.disconnect();
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
}, [client, externalClient]);
|
|
28
|
+
return /* @__PURE__ */ jsx(BaseContext, { value: client, children });
|
|
29
|
+
}
|
|
30
|
+
function useBase() {
|
|
31
|
+
const client = useContext(BaseContext);
|
|
32
|
+
if (!client) {
|
|
33
|
+
throw new Error("useBase: must be used within a <BaseProvider>");
|
|
34
|
+
}
|
|
35
|
+
return client;
|
|
36
|
+
}
|
|
37
|
+
function useQuery(collection, options) {
|
|
38
|
+
const client = useBase();
|
|
39
|
+
const [data, setData] = useState([]);
|
|
40
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
41
|
+
const [error, setError] = useState(null);
|
|
42
|
+
const filter = options?.filter ?? "";
|
|
43
|
+
const sort = options?.sort;
|
|
44
|
+
const expand = options?.expand;
|
|
45
|
+
const fields = options?.fields;
|
|
46
|
+
const page = options?.page;
|
|
47
|
+
const perPage = options?.perPage;
|
|
48
|
+
const realtimeEnabled = options?.realtime !== false;
|
|
49
|
+
const enabled = options?.enabled !== false;
|
|
50
|
+
const fetchData = useCallback(async () => {
|
|
51
|
+
if (!enabled) return;
|
|
52
|
+
setIsLoading(true);
|
|
53
|
+
setError(null);
|
|
54
|
+
try {
|
|
55
|
+
const result = await client.list(collection, {
|
|
56
|
+
filter,
|
|
57
|
+
sort,
|
|
58
|
+
expand,
|
|
59
|
+
fields,
|
|
60
|
+
page,
|
|
61
|
+
perPage
|
|
62
|
+
});
|
|
63
|
+
setData(result.items);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
setError(err instanceof Error ? err : new Error(String(err)));
|
|
66
|
+
} finally {
|
|
67
|
+
setIsLoading(false);
|
|
68
|
+
}
|
|
69
|
+
}, [client, collection, filter, sort, expand, fields, page, perPage, enabled]);
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
fetchData();
|
|
72
|
+
}, [fetchData]);
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!enabled) return;
|
|
75
|
+
const unsubscribe = client.store.subscribe(collection, filter, (records) => {
|
|
76
|
+
setData(records);
|
|
77
|
+
});
|
|
78
|
+
return unsubscribe;
|
|
79
|
+
}, [client, collection, filter, enabled]);
|
|
80
|
+
useEffect(() => {
|
|
81
|
+
if (!realtimeEnabled || !enabled) return;
|
|
82
|
+
const unsubscribe = client.subscribeAndSync(collection, "*");
|
|
83
|
+
return unsubscribe;
|
|
84
|
+
}, [client, collection, realtimeEnabled, enabled]);
|
|
85
|
+
return { data, isLoading, error, refetch: fetchData };
|
|
86
|
+
}
|
|
87
|
+
function useMutation(collection, action) {
|
|
88
|
+
const client = useBase();
|
|
89
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
90
|
+
const [error, setError] = useState(null);
|
|
91
|
+
const mutate = useCallback(
|
|
92
|
+
async (data, opts) => {
|
|
93
|
+
setIsLoading(true);
|
|
94
|
+
setError(null);
|
|
95
|
+
let optimisticId;
|
|
96
|
+
try {
|
|
97
|
+
if (opts?.optimistic && action !== "delete") {
|
|
98
|
+
const tempRecord = {
|
|
99
|
+
id: data.id ?? `_temp_${Date.now()}`,
|
|
100
|
+
...data
|
|
101
|
+
};
|
|
102
|
+
optimisticId = client.store.optimisticSet(collection, tempRecord);
|
|
103
|
+
} else if (opts?.optimistic && action === "delete" && data.id) {
|
|
104
|
+
optimisticId = client.store.optimisticDelete(collection, data.id);
|
|
105
|
+
}
|
|
106
|
+
let result = void 0;
|
|
107
|
+
switch (action) {
|
|
108
|
+
case "create":
|
|
109
|
+
result = await client.create(collection, data);
|
|
110
|
+
break;
|
|
111
|
+
case "update": {
|
|
112
|
+
const id = data.id;
|
|
113
|
+
if (!id) throw new Error("useMutation: update requires data.id");
|
|
114
|
+
const { id: _id, ...rest } = data;
|
|
115
|
+
result = await client.update(collection, id, rest);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case "delete": {
|
|
119
|
+
const id = data.id;
|
|
120
|
+
if (!id) throw new Error("useMutation: delete requires data.id");
|
|
121
|
+
await client.delete(collection, id);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (optimisticId) {
|
|
126
|
+
client.store.rollbackOptimistic(optimisticId);
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
} catch (err) {
|
|
130
|
+
if (optimisticId) {
|
|
131
|
+
client.store.rollbackOptimistic(optimisticId);
|
|
132
|
+
}
|
|
133
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
134
|
+
setError(e);
|
|
135
|
+
throw e;
|
|
136
|
+
} finally {
|
|
137
|
+
setIsLoading(false);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
[client, collection, action]
|
|
141
|
+
);
|
|
142
|
+
return { mutate, isLoading, error };
|
|
143
|
+
}
|
|
144
|
+
function useAuth(collection = "users") {
|
|
145
|
+
const client = useBase();
|
|
146
|
+
const [user, setUser] = useState(client.authStore.record);
|
|
147
|
+
const [token, setToken] = useState(client.authStore.token);
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
const unsubscribe = client.authStore.onChange((newToken, newRecord) => {
|
|
150
|
+
setToken(newToken);
|
|
151
|
+
setUser(newRecord);
|
|
152
|
+
});
|
|
153
|
+
return unsubscribe;
|
|
154
|
+
}, [client]);
|
|
155
|
+
const signIn = useCallback(
|
|
156
|
+
async (identity, password) => {
|
|
157
|
+
await client.signInWithPassword(collection, identity, password);
|
|
158
|
+
},
|
|
159
|
+
[client, collection]
|
|
160
|
+
);
|
|
161
|
+
const signUp = useCallback(
|
|
162
|
+
async (data) => {
|
|
163
|
+
return client.signUp(collection, data);
|
|
164
|
+
},
|
|
165
|
+
[client, collection]
|
|
166
|
+
);
|
|
167
|
+
const signOut = useCallback(() => {
|
|
168
|
+
client.signOut();
|
|
169
|
+
}, [client]);
|
|
170
|
+
return {
|
|
171
|
+
user,
|
|
172
|
+
token,
|
|
173
|
+
isValid: client.authStore.isValid,
|
|
174
|
+
signIn,
|
|
175
|
+
signUp,
|
|
176
|
+
signOut,
|
|
177
|
+
onChange: client.authStore.onChange.bind(client.authStore)
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function useRealtime(collection, topic, callback) {
|
|
181
|
+
const client = useBase();
|
|
182
|
+
const callbackRef = useRef(callback);
|
|
183
|
+
callbackRef.current = callback;
|
|
184
|
+
useEffect(() => {
|
|
185
|
+
const unsubscribe = client.realtime.subscribe(collection, topic, (event) => {
|
|
186
|
+
callbackRef.current(event);
|
|
187
|
+
});
|
|
188
|
+
return unsubscribe;
|
|
189
|
+
}, [client, collection, topic]);
|
|
190
|
+
}
|
|
191
|
+
function useConnectionState() {
|
|
192
|
+
const client = useBase();
|
|
193
|
+
const [state, setState] = useState(client.realtime.state);
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
const unsubscribe = client.realtime.onConnectionChange((s) => {
|
|
196
|
+
setState(s);
|
|
197
|
+
});
|
|
198
|
+
return unsubscribe;
|
|
199
|
+
}, [client]);
|
|
200
|
+
return state;
|
|
201
|
+
}
|
|
202
|
+
function usePresence(sync) {
|
|
203
|
+
const [peers, setPeers] = useState(/* @__PURE__ */ new Map());
|
|
204
|
+
const [myState, setMyState] = useState({});
|
|
205
|
+
useEffect(() => {
|
|
206
|
+
const unsubscribe = sync.onPeersChange((p) => {
|
|
207
|
+
setPeers(new Map(p));
|
|
208
|
+
});
|
|
209
|
+
return unsubscribe;
|
|
210
|
+
}, [sync]);
|
|
211
|
+
const updateState = useCallback(
|
|
212
|
+
(meta) => {
|
|
213
|
+
setMyState(meta);
|
|
214
|
+
sync.updatePresence(meta);
|
|
215
|
+
},
|
|
216
|
+
[sync]
|
|
217
|
+
);
|
|
218
|
+
return { peers, myState, updateState };
|
|
219
|
+
}
|
|
220
|
+
function useCRDT(documentId, wsUrl) {
|
|
221
|
+
const client = useBase();
|
|
222
|
+
const docRef = useRef(null);
|
|
223
|
+
const syncRef = useRef(null);
|
|
224
|
+
const [syncState, setSyncState] = useState("disconnected");
|
|
225
|
+
if (!docRef.current || docRef.current.id !== documentId) {
|
|
226
|
+
docRef.current = new CRDTDocument(documentId);
|
|
227
|
+
}
|
|
228
|
+
if (!syncRef.current) {
|
|
229
|
+
syncRef.current = new CRDTSync();
|
|
230
|
+
}
|
|
231
|
+
const doc = docRef.current;
|
|
232
|
+
const sync = syncRef.current;
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
const url = wsUrl ?? deriveCrdtWsUrl(client.url);
|
|
235
|
+
const token = client.authStore.token || void 0;
|
|
236
|
+
sync.connect(url, doc, token);
|
|
237
|
+
const unsubState = sync.onStateChange((s) => {
|
|
238
|
+
setSyncState(s);
|
|
239
|
+
});
|
|
240
|
+
return () => {
|
|
241
|
+
unsubState();
|
|
242
|
+
sync.disconnect();
|
|
243
|
+
};
|
|
244
|
+
}, [doc, sync, client, wsUrl]);
|
|
245
|
+
const text = useCallback((field) => doc.getText(field), [doc]);
|
|
246
|
+
const counter = useCallback((field) => doc.getCounter(field), [doc]);
|
|
247
|
+
const set = useCallback((field) => doc.getSet(field), [doc]);
|
|
248
|
+
const register = useCallback((field) => doc.getRegister(field), [doc]);
|
|
249
|
+
return { doc, text, counter, set, register, sync, syncState };
|
|
250
|
+
}
|
|
251
|
+
function useCRDTText(doc, field) {
|
|
252
|
+
const textCrdt = doc.getText(field);
|
|
253
|
+
const [value, setValue] = useState(() => textCrdt.toString());
|
|
254
|
+
useEffect(() => {
|
|
255
|
+
const unsubscribe = textCrdt.onChange((text) => {
|
|
256
|
+
setValue(text);
|
|
257
|
+
});
|
|
258
|
+
return unsubscribe;
|
|
259
|
+
}, [textCrdt]);
|
|
260
|
+
return value;
|
|
261
|
+
}
|
|
262
|
+
function useCRDTCounter(doc, field) {
|
|
263
|
+
const counterCrdt = doc.getCounter(field);
|
|
264
|
+
const [value, setValue] = useState(() => counterCrdt.value);
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
const unsubscribe = counterCrdt.onChange((v) => {
|
|
267
|
+
setValue(v);
|
|
268
|
+
});
|
|
269
|
+
return unsubscribe;
|
|
270
|
+
}, [counterCrdt]);
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
273
|
+
function deriveCrdtWsUrl(httpUrl) {
|
|
274
|
+
const url = httpUrl.replace(/\/$/, "");
|
|
275
|
+
if (url.startsWith("https://")) {
|
|
276
|
+
return url.replace("https://", "wss://") + "/api/crdt";
|
|
277
|
+
}
|
|
278
|
+
return url.replace("http://", "ws://") + "/api/crdt";
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export { BaseProvider, useAuth, useBase, useCRDT, useCRDTCounter, useCRDTText, useConnectionState, useMutation, usePresence, useQuery, useRealtime };
|
|
282
|
+
//# sourceMappingURL=index.js.map
|
|
283
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/react/context.tsx","../../src/react/hooks.ts"],"names":["useEffect","useRef"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * BaseProvider -- React context providing the BaseClient instance.\n *\n * Usage:\n * <BaseProvider url=\"https://myapp.hanzo.ai\">\n * <App />\n * </BaseProvider>\n *\n * // or with an existing client:\n * <BaseProvider client={myClient}>\n * <App />\n * </BaseProvider>\n */\n\nimport { createContext, useContext, useRef, useEffect, type ReactNode } from 'react'\nimport { BaseClient, type AuthStore, type ClientConfig } from '../core/client.js'\n\n// ---------------------------------------------------------------------------\n// Context\n// ---------------------------------------------------------------------------\n\nconst BaseContext = createContext<BaseClient | null>(null)\n\n// ---------------------------------------------------------------------------\n// Provider props\n// ---------------------------------------------------------------------------\n\nexport interface BaseProviderProps {\n /** Base URL for automatic client creation. */\n url?: string\n /** Optional auth store override. */\n authStore?: AuthStore\n /** Pre-created BaseClient (takes precedence over url). */\n client?: BaseClient\n children: ReactNode\n}\n\n// ---------------------------------------------------------------------------\n// Provider\n// ---------------------------------------------------------------------------\n\nexport function BaseProvider({ url, authStore, client: externalClient, children }: BaseProviderProps) {\n // Use a ref to hold the client so it survives re-renders without\n // creating a new client on every render.\n const clientRef = useRef<BaseClient | null>(null)\n\n if (externalClient) {\n clientRef.current = externalClient\n } else if (!clientRef.current && url) {\n const config: ClientConfig = { url }\n if (authStore) config.authStore = authStore\n clientRef.current = new BaseClient(config)\n }\n\n const client = clientRef.current\n if (!client) {\n throw new Error('BaseProvider: provide either `url` or `client` prop')\n }\n\n // Cleanup: disconnect realtime on unmount.\n useEffect(() => {\n return () => {\n // Only disconnect if we own the client (created from url).\n if (!externalClient) {\n client.disconnect()\n }\n }\n }, [client, externalClient])\n\n return <BaseContext value={client}>{children}</BaseContext>\n}\n\n// ---------------------------------------------------------------------------\n// Hook\n// ---------------------------------------------------------------------------\n\n/**\n * Get the BaseClient from the nearest BaseProvider.\n * Throws if used outside a provider.\n */\nexport function useBase(): BaseClient {\n const client = useContext(BaseContext)\n if (!client) {\n throw new Error('useBase: must be used within a <BaseProvider>')\n }\n return client\n}\n","/**\n * React hooks for Hanzo Base.\n *\n * All hooks use the BaseClient from the nearest BaseProvider context.\n * They manage subscriptions, cleanup, and state transitions correctly\n * with React 19 patterns (useEffect, useState, useCallback, useRef).\n */\n\nimport {\n useState,\n useEffect,\n useCallback,\n useRef,\n} from 'react'\n\nimport { useBase } from './context.js'\nimport type { BaseRecord } from '../core/state.js'\nimport type { ListOptions, AuthStore } from '../core/client.js'\nimport type { RealtimeEvent, ConnectionState } from '../core/realtime.js'\nimport type { CRDTDocument } from '../crdt/document.js'\nimport type { CRDTText } from '../crdt/text.js'\nimport type { CRDTCounter } from '../crdt/counter.js'\nimport type { CRDTSet } from '../crdt/set.js'\nimport type { CRDTRegister } from '../crdt/register.js'\nimport type { PeerState, SyncState } from '../crdt/sync.js'\nimport { CRDTSync } from '../crdt/sync.js'\nimport { CRDTDocument as CRDTDocumentImpl } from '../crdt/document.js'\n\n// ---------------------------------------------------------------------------\n// useQuery -- reactive collection query with SSE subscription\n// ---------------------------------------------------------------------------\n\nexport interface UseQueryOptions extends ListOptions {\n /** Whether to auto-subscribe to realtime updates. Default: true. */\n realtime?: boolean\n /** Whether to run the query immediately. Default: true. */\n enabled?: boolean\n}\n\nexport interface UseQueryResult<T = BaseRecord> {\n data: T[]\n isLoading: boolean\n error: Error | null\n /** Manually refetch the query. */\n refetch: () => Promise<void>\n}\n\n/**\n * Reactive query hook.\n *\n * Fetches records from a collection and subscribes to realtime updates.\n * Results are cached in the QueryStore for deduplication across components.\n */\nexport function useQuery<T extends BaseRecord = BaseRecord>(\n collection: string,\n options?: UseQueryOptions,\n): UseQueryResult<T> {\n const client = useBase()\n const [data, setData] = useState<T[]>([])\n const [isLoading, setIsLoading] = useState(true)\n const [error, setError] = useState<Error | null>(null)\n\n // Stabilize options reference.\n const filter = options?.filter ?? ''\n const sort = options?.sort\n const expand = options?.expand\n const fields = options?.fields\n const page = options?.page\n const perPage = options?.perPage\n const realtimeEnabled = options?.realtime !== false\n const enabled = options?.enabled !== false\n\n const fetchData = useCallback(async () => {\n if (!enabled) return\n setIsLoading(true)\n setError(null)\n try {\n const result = await client.list(collection, {\n filter, sort, expand, fields, page, perPage,\n })\n setData(result.items as T[])\n } catch (err) {\n setError(err instanceof Error ? err : new Error(String(err)))\n } finally {\n setIsLoading(false)\n }\n }, [client, collection, filter, sort, expand, fields, page, perPage, enabled])\n\n // Initial fetch.\n useEffect(() => {\n fetchData()\n }, [fetchData])\n\n // Subscribe to QueryStore for optimistic + server updates.\n useEffect(() => {\n if (!enabled) return\n\n const unsubscribe = client.store.subscribe(collection, filter, (records) => {\n setData(records as T[])\n })\n\n return unsubscribe\n }, [client, collection, filter, enabled])\n\n // SSE realtime subscription.\n useEffect(() => {\n if (!realtimeEnabled || !enabled) return\n\n const unsubscribe = client.subscribeAndSync(collection, '*')\n return unsubscribe\n }, [client, collection, realtimeEnabled, enabled])\n\n return { data, isLoading, error, refetch: fetchData }\n}\n\n// ---------------------------------------------------------------------------\n// useMutation -- mutation with optimistic update support\n// ---------------------------------------------------------------------------\n\nexport type MutationAction = 'create' | 'update' | 'delete'\n\nexport interface MutateOptions {\n /** If true, apply an optimistic update to the QueryStore before the server responds. */\n optimistic?: boolean\n}\n\nexport interface UseMutationResult {\n mutate: (data: Record<string, unknown>, options?: MutateOptions) => Promise<BaseRecord | void>\n isLoading: boolean\n error: Error | null\n}\n\n/**\n * Mutation hook.\n *\n * Performs a create, update, or delete mutation against a collection.\n * Supports optimistic updates that are automatically rolled back on failure.\n */\nexport function useMutation(\n collection: string,\n action: MutationAction,\n): UseMutationResult {\n const client = useBase()\n const [isLoading, setIsLoading] = useState(false)\n const [error, setError] = useState<Error | null>(null)\n\n const mutate = useCallback(\n async (data: Record<string, unknown>, opts?: MutateOptions): Promise<BaseRecord | void> => {\n setIsLoading(true)\n setError(null)\n\n let optimisticId: string | undefined\n\n try {\n // Optimistic update.\n if (opts?.optimistic && action !== 'delete') {\n const tempRecord: BaseRecord = {\n id: data.id as string ?? `_temp_${Date.now()}`,\n ...data,\n }\n optimisticId = client.store.optimisticSet(collection, tempRecord)\n } else if (opts?.optimistic && action === 'delete' && data.id) {\n optimisticId = client.store.optimisticDelete(collection, data.id as string)\n }\n\n // Server request.\n let result: BaseRecord | void = undefined\n switch (action) {\n case 'create':\n result = await client.create(collection, data)\n break\n case 'update': {\n const id = data.id as string\n if (!id) throw new Error('useMutation: update requires data.id')\n const { id: _id, ...rest } = data\n result = await client.update(collection, id, rest)\n break\n }\n case 'delete': {\n const id = data.id as string\n if (!id) throw new Error('useMutation: delete requires data.id')\n await client.delete(collection, id)\n break\n }\n }\n\n // On success, remove optimistic entry (server truth takes over via applyServerUpdate).\n if (optimisticId) {\n client.store.rollbackOptimistic(optimisticId)\n }\n\n return result\n } catch (err) {\n // Rollback optimistic on failure.\n if (optimisticId) {\n client.store.rollbackOptimistic(optimisticId)\n }\n const e = err instanceof Error ? err : new Error(String(err))\n setError(e)\n throw e\n } finally {\n setIsLoading(false)\n }\n },\n [client, collection, action],\n )\n\n return { mutate, isLoading, error }\n}\n\n// ---------------------------------------------------------------------------\n// useAuth -- auth state\n// ---------------------------------------------------------------------------\n\nexport interface UseAuthResult {\n user: BaseRecord | null\n token: string\n isValid: boolean\n signIn: (identity: string, password: string) => Promise<void>\n signUp: (data: Record<string, unknown>) => Promise<BaseRecord>\n signOut: () => void\n /** Subscribe to auth changes. Returns unsubscribe. */\n onChange: AuthStore['onChange']\n}\n\n/**\n * Auth state hook.\n *\n * Returns the current auth state and provides sign-in, sign-up, and\n * sign-out methods. Re-renders when auth state changes.\n *\n * @param collection - Auth collection name (default: \"users\").\n */\nexport function useAuth(collection = 'users'): UseAuthResult {\n const client = useBase()\n const [user, setUser] = useState<BaseRecord | null>(client.authStore.record)\n const [token, setToken] = useState(client.authStore.token)\n\n useEffect(() => {\n const unsubscribe = client.authStore.onChange((newToken, newRecord) => {\n setToken(newToken)\n setUser(newRecord)\n })\n return unsubscribe\n }, [client])\n\n const signIn = useCallback(\n async (identity: string, password: string) => {\n await client.signInWithPassword(collection, identity, password)\n },\n [client, collection],\n )\n\n const signUp = useCallback(\n async (data: Record<string, unknown>) => {\n return client.signUp(collection, data)\n },\n [client, collection],\n )\n\n const signOut = useCallback(() => {\n client.signOut()\n }, [client])\n\n return {\n user,\n token,\n isValid: client.authStore.isValid,\n signIn,\n signUp,\n signOut,\n onChange: client.authStore.onChange.bind(client.authStore),\n }\n}\n\n// ---------------------------------------------------------------------------\n// useRealtime -- low-level SSE subscription\n// ---------------------------------------------------------------------------\n\n/**\n * Low-level realtime subscription hook.\n *\n * Subscribes to SSE events for a collection topic on mount and\n * unsubscribes on unmount.\n */\nexport function useRealtime(\n collection: string,\n topic: string,\n callback: (event: RealtimeEvent) => void,\n): void {\n const client = useBase()\n const callbackRef = useRef(callback)\n callbackRef.current = callback\n\n useEffect(() => {\n const unsubscribe = client.realtime.subscribe(collection, topic, (event) => {\n callbackRef.current(event)\n })\n return unsubscribe\n }, [client, collection, topic])\n}\n\n// ---------------------------------------------------------------------------\n// useConnectionState -- realtime connection state\n// ---------------------------------------------------------------------------\n\nexport function useConnectionState(): ConnectionState {\n const client = useBase()\n const [state, setState] = useState<ConnectionState>(client.realtime.state)\n\n useEffect(() => {\n const unsubscribe = client.realtime.onConnectionChange((s) => {\n setState(s)\n })\n return unsubscribe\n }, [client])\n\n return state\n}\n\n// ---------------------------------------------------------------------------\n// usePresence -- presence tracking via CRDT sync\n// ---------------------------------------------------------------------------\n\nexport interface UsePresenceResult {\n /** Map of peer siteId to their state. */\n peers: Map<string, PeerState>\n /** Our own presence metadata. */\n myState: Record<string, unknown>\n /** Update our presence metadata. */\n updateState: (meta: Record<string, unknown>) => void\n}\n\n/**\n * Presence tracking hook.\n *\n * Requires a CRDTSync instance (usually obtained from useCRDT).\n * Tracks peers connected to the same document.\n */\nexport function usePresence(sync: CRDTSync): UsePresenceResult {\n const [peers, setPeers] = useState<Map<string, PeerState>>(new Map())\n const [myState, setMyState] = useState<Record<string, unknown>>({})\n\n useEffect(() => {\n const unsubscribe = sync.onPeersChange((p) => {\n setPeers(new Map(p))\n })\n return unsubscribe\n }, [sync])\n\n const updateState = useCallback(\n (meta: Record<string, unknown>) => {\n setMyState(meta)\n sync.updatePresence(meta)\n },\n [sync],\n )\n\n return { peers, myState, updateState }\n}\n\n// ---------------------------------------------------------------------------\n// useCRDT -- CRDT document hook with auto-sync\n// ---------------------------------------------------------------------------\n\nexport interface UseCRDTResult {\n /** The CRDTDocument instance. */\n doc: CRDTDocument\n /** Convenience accessor for text fields. */\n text: (field: string) => CRDTText\n /** Convenience accessor for counter fields. */\n counter: (field: string) => CRDTCounter\n /** Convenience accessor for set fields. */\n set: <T = unknown>(field: string) => CRDTSet<T>\n /** Convenience accessor for register fields. */\n register: <T = unknown>(field: string) => CRDTRegister<T>\n /** The CRDTSync instance for advanced usage (presence, state listeners). */\n sync: CRDTSync\n /** Current sync state. */\n syncState: SyncState\n}\n\n/**\n * CRDT document hook.\n *\n * Creates a CRDTDocument and CRDTSync, connects to the server via\n * WebSocket, and auto-syncs all local operations.\n *\n * @param documentId - Unique document identifier.\n * @param wsUrl - WebSocket URL for the CRDT sync endpoint. If not provided,\n * derives it from the BaseClient URL (http->ws, https->wss).\n */\nexport function useCRDT(documentId: string, wsUrl?: string): UseCRDTResult {\n const client = useBase()\n\n // Stable refs for document and sync -- created once per documentId.\n const docRef = useRef<CRDTDocument | null>(null)\n const syncRef = useRef<CRDTSync | null>(null)\n const [syncState, setSyncState] = useState<SyncState>('disconnected')\n\n // Create document and sync on first render or documentId change.\n if (!docRef.current || docRef.current.id !== documentId) {\n docRef.current = new CRDTDocumentImpl(documentId)\n }\n if (!syncRef.current) {\n syncRef.current = new CRDTSync()\n }\n\n const doc = docRef.current\n const sync = syncRef.current\n\n // Connect on mount, disconnect on unmount.\n useEffect(() => {\n const url = wsUrl ?? deriveCrdtWsUrl(client.url)\n const token = client.authStore.token || undefined\n\n sync.connect(url, doc, token)\n\n const unsubState = sync.onStateChange((s) => {\n setSyncState(s)\n })\n\n return () => {\n unsubState()\n sync.disconnect()\n }\n }, [doc, sync, client, wsUrl])\n\n // Convenience accessors that delegate to the document.\n const text = useCallback((field: string) => doc.getText(field), [doc])\n const counter = useCallback((field: string) => doc.getCounter(field), [doc])\n const set = useCallback(<T = unknown>(field: string) => doc.getSet<T>(field), [doc])\n const register = useCallback(<T = unknown>(field: string) => doc.getRegister<T>(field), [doc])\n\n return { doc, text, counter, set, register, sync, syncState }\n}\n\n// ---------------------------------------------------------------------------\n// useCRDTText -- subscribe to a CRDT text field's content\n// ---------------------------------------------------------------------------\n\n/**\n * Subscribe to a specific CRDTText field's current string value.\n * Re-renders whenever the text changes (local or remote).\n */\nexport function useCRDTText(doc: CRDTDocument, field: string): string {\n const textCrdt = doc.getText(field)\n const [value, setValue] = useState(() => textCrdt.toString())\n\n useEffect(() => {\n const unsubscribe = textCrdt.onChange((text) => {\n setValue(text)\n })\n return unsubscribe\n }, [textCrdt])\n\n return value\n}\n\n// ---------------------------------------------------------------------------\n// useCRDTCounter -- subscribe to a CRDT counter field\n// ---------------------------------------------------------------------------\n\nexport function useCRDTCounter(doc: CRDTDocument, field: string): number {\n const counterCrdt = doc.getCounter(field)\n const [value, setValue] = useState(() => counterCrdt.value)\n\n useEffect(() => {\n const unsubscribe = counterCrdt.onChange((v) => {\n setValue(v)\n })\n return unsubscribe\n }, [counterCrdt])\n\n return value\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/** Derive WebSocket URL from HTTP URL: https://x -> wss://x/api/crdt */\nfunction deriveCrdtWsUrl(httpUrl: string): string {\n const url = httpUrl.replace(/\\/$/, '')\n if (url.startsWith('https://')) {\n return url.replace('https://', 'wss://') + '/api/crdt'\n }\n return url.replace('http://', 'ws://') + '/api/crdt'\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hanzo/base",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Hanzo Base SDK - Reactive queries, CRDT, realtime, and PocketBase-compatible client",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/core/index.js",
|
|
7
|
+
"module": "./dist/core/index.js",
|
|
8
|
+
"types": "./dist/core/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/core/index.js",
|
|
12
|
+
"types": "./dist/core/index.d.ts"
|
|
13
|
+
},
|
|
14
|
+
"./compat": {
|
|
15
|
+
"import": "./dist/compat/index.js",
|
|
16
|
+
"types": "./dist/compat/index.d.ts"
|
|
17
|
+
},
|
|
18
|
+
"./react": {
|
|
19
|
+
"import": "./dist/react/index.js",
|
|
20
|
+
"types": "./dist/react/index.d.ts"
|
|
21
|
+
},
|
|
22
|
+
"./crdt": {
|
|
23
|
+
"import": "./dist/crdt/index.js",
|
|
24
|
+
"types": "./dist/crdt/index.d.ts"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"src"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsup",
|
|
33
|
+
"dev": "tsup --watch",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
35
|
+
"clean": "rm -rf dist",
|
|
36
|
+
"prepublishOnly": "npm run build"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"pocketbase": "^0.26.5"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/react": "^19.0.0",
|
|
43
|
+
"react": "^19.0.0",
|
|
44
|
+
"tsup": "^8.0.0",
|
|
45
|
+
"typescript": "^5.4.0"
|
|
46
|
+
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"react": ">=18.0.0"
|
|
49
|
+
},
|
|
50
|
+
"peerDependenciesMeta": {
|
|
51
|
+
"react": {
|
|
52
|
+
"optional": true
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"license": "MIT",
|
|
56
|
+
"repository": {
|
|
57
|
+
"type": "git",
|
|
58
|
+
"url": "https://github.com/hanzoai/base.git",
|
|
59
|
+
"directory": "sdk/base-js"
|
|
60
|
+
},
|
|
61
|
+
"keywords": [
|
|
62
|
+
"hanzo",
|
|
63
|
+
"base",
|
|
64
|
+
"sdk",
|
|
65
|
+
"backend",
|
|
66
|
+
"realtime",
|
|
67
|
+
"reactive",
|
|
68
|
+
"crdt",
|
|
69
|
+
"pocketbase"
|
|
70
|
+
]
|
|
71
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hanzoai/base/compat -- PocketBase-compatible client re-exports.
|
|
3
|
+
*
|
|
4
|
+
* Provides a drop-in replacement for the `pocketbase` npm package
|
|
5
|
+
* so that existing code can migrate to `@hanzoai/base` without changes.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import Base, { LocalAuthStore, isTokenExpired } from '@hanzoai/base/compat'
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
default,
|
|
13
|
+
default as PocketBase,
|
|
14
|
+
default as Base,
|
|
15
|
+
LocalAuthStore,
|
|
16
|
+
AsyncAuthStore,
|
|
17
|
+
BaseAuthStore,
|
|
18
|
+
isTokenExpired,
|
|
19
|
+
ClientResponseError,
|
|
20
|
+
cookieParse,
|
|
21
|
+
cookieSerialize,
|
|
22
|
+
getTokenPayload,
|
|
23
|
+
normalizeUnknownQueryParams,
|
|
24
|
+
} from 'pocketbase'
|