@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.
@@ -0,0 +1,372 @@
1
+ /**
2
+ * Hybrid logical clock for CRDT operation ordering.
3
+ *
4
+ * Each operation is tagged with (timestamp, counter, siteId) to provide
5
+ * a total order across all sites. Compatible with the Go HLC implementation.
6
+ */
7
+ interface HLCTimestamp {
8
+ /** Wall-clock milliseconds since epoch. */
9
+ ts: number;
10
+ /** Monotonic counter to break ties within the same ms. */
11
+ counter: number;
12
+ /** Unique site identifier (usually client session id). */
13
+ siteId: string;
14
+ }
15
+ declare class HybridLogicalClock {
16
+ private _ts;
17
+ private _counter;
18
+ readonly siteId: string;
19
+ constructor(siteId?: string);
20
+ /** Generate a new timestamp, guaranteed > any previous. */
21
+ now(): HLCTimestamp;
22
+ /** Receive a remote timestamp and merge with local clock. */
23
+ receive(remote: HLCTimestamp): HLCTimestamp;
24
+ }
25
+ /** Compare two HLC timestamps. Returns <0, 0, or >0. */
26
+ declare function compareHLC(a: HLCTimestamp, b: HLCTimestamp): number;
27
+
28
+ /**
29
+ * CRDT operation types -- wire format for sync between client and server.
30
+ *
31
+ * All operations are serializable to JSON and designed for
32
+ * eventual compatibility with a Go CRDT server.
33
+ */
34
+
35
+ type OperationType = 'text.insert' | 'text.delete' | 'counter.increment' | 'set.add' | 'set.remove' | 'register.set';
36
+ interface Operation {
37
+ /** Unique operation id: `{siteId}:{ts}:{counter}` */
38
+ id: string;
39
+ /** The CRDT document id this operation belongs to. */
40
+ documentId: string;
41
+ /** Which field within the document. */
42
+ field: string;
43
+ /** HLC timestamp for causal ordering. */
44
+ hlc: HLCTimestamp;
45
+ /** Operation type tag. */
46
+ type: OperationType;
47
+ /** Type-specific payload. */
48
+ payload: OperationPayload;
49
+ }
50
+ interface TextInsertPayload {
51
+ /** Position id of the character after which to insert (null = head). */
52
+ afterId: string | null;
53
+ /** The text content to insert. Each character gets its own positional id. */
54
+ content: string;
55
+ /** Assigned position ids for each character (same length as content). */
56
+ positionIds: string[];
57
+ }
58
+ interface TextDeletePayload {
59
+ /** Position ids of the characters to tombstone. */
60
+ positionIds: string[];
61
+ }
62
+ interface CounterIncrementPayload {
63
+ /** Delta (positive for increment, negative for decrement). */
64
+ delta: number;
65
+ }
66
+ interface SetAddPayload {
67
+ /** The value to add (JSON-serializable). */
68
+ value: unknown;
69
+ /** Unique tag for this add operation (for OR-Set semantics). */
70
+ tag: string;
71
+ }
72
+ interface SetRemovePayload {
73
+ /** The value to remove. */
74
+ value: unknown;
75
+ /** Tags being causally removed (observed tags at removal time). */
76
+ tags: string[];
77
+ }
78
+ interface RegisterSetPayload {
79
+ /** The new value. */
80
+ value: unknown;
81
+ }
82
+ type OperationPayload = TextInsertPayload | TextDeletePayload | CounterIncrementPayload | SetAddPayload | SetRemovePayload | RegisterSetPayload;
83
+ declare function makeOpId(hlc: HLCTimestamp): string;
84
+ declare function makePositionId(hlc: HLCTimestamp, charIndex: number): string;
85
+
86
+ /**
87
+ * CRDTText -- collaborative text based on RGA (Replicated Growable Array).
88
+ *
89
+ * Each character has a unique position id. Insertions reference the position
90
+ * id of the character they follow. Deletions tombstone position ids.
91
+ * The structure resolves conflicts by using HLC comparison on concurrent
92
+ * inserts at the same position.
93
+ */
94
+
95
+ type TextChangeCallback = (text: string) => void;
96
+ declare class CRDTText {
97
+ private _nodes;
98
+ private readonly _clock;
99
+ private readonly _documentId;
100
+ private readonly _field;
101
+ private readonly _pendingOps;
102
+ private _listeners;
103
+ /** Index from position id to array index for O(1) lookup. */
104
+ private _index;
105
+ constructor(documentId: string, field: string, clock: HybridLogicalClock);
106
+ /** Insert text at a visible character position (0-based). */
107
+ insert(position: number, text: string): Operation;
108
+ /** Delete `length` visible characters starting at `position` (0-based). */
109
+ delete(position: number, length: number): Operation;
110
+ /** Apply a remote operation. */
111
+ applyRemote(op: Operation): void;
112
+ /** Get the current visible text. */
113
+ toString(): string;
114
+ /** Number of visible characters. */
115
+ get length(): number;
116
+ /** Drain pending local operations. */
117
+ drainOps(): Operation[];
118
+ onChange(callback: TextChangeCallback): () => void;
119
+ /**
120
+ * Insert a node into the correct position in the array.
121
+ * RGA rule: among siblings sharing the same afterId, order by HLC descending
122
+ * (newer inserts appear first, pushing older ones right).
123
+ */
124
+ private _insertNode;
125
+ /** Rebuild the id->index map from `startIdx` onward. */
126
+ private _rebuildIndex;
127
+ /** Get the position id of the visible character at position `pos`, or null for before-head. */
128
+ private _visibleIdAtPosition;
129
+ /** Get position ids for `length` visible characters starting at `position`. */
130
+ private _visibleIdsInRange;
131
+ private _notifyChange;
132
+ }
133
+
134
+ /**
135
+ * CRDTCounter -- PN-Counter (positive-negative counter).
136
+ *
137
+ * Each site tracks its own positive and negative totals.
138
+ * The value is sum(positives) - sum(negatives) across all sites.
139
+ * Merge is max() per-site per-direction.
140
+ */
141
+
142
+ type CounterChangeCallback = (value: number) => void;
143
+ declare class CRDTCounter {
144
+ /** Per-site positive accumulator. */
145
+ private _positive;
146
+ /** Per-site negative accumulator. */
147
+ private _negative;
148
+ private readonly _clock;
149
+ private readonly _documentId;
150
+ private readonly _field;
151
+ private readonly _pendingOps;
152
+ private _listeners;
153
+ constructor(documentId: string, field: string, clock: HybridLogicalClock);
154
+ get value(): number;
155
+ increment(amount?: number): Operation;
156
+ decrement(amount?: number): Operation;
157
+ /** Apply a remote increment operation. */
158
+ applyRemote(op: Operation): void;
159
+ /** Merge with a remote counter state (for full-state sync). */
160
+ mergeState(positives: Record<string, number>, negatives: Record<string, number>): void;
161
+ /** Export state for full-state sync. */
162
+ exportState(): {
163
+ positive: Record<string, number>;
164
+ negative: Record<string, number>;
165
+ };
166
+ drainOps(): Operation[];
167
+ onChange(callback: CounterChangeCallback): () => void;
168
+ private _notifyChange;
169
+ }
170
+
171
+ /**
172
+ * CRDTSet<T> -- Observed-Remove Set (OR-Set).
173
+ *
174
+ * Each add() generates a unique tag. A remove() records the set of tags
175
+ * observed for that value at removal time. The value is present iff it has
176
+ * any tag not covered by a remove.
177
+ *
178
+ * This gives add-wins semantics: a concurrent add and remove of the same
179
+ * value results in the value being present (the new add's tag survives).
180
+ */
181
+
182
+ type SetChangeCallback<T> = (values: T[]) => void;
183
+ declare class CRDTSet<T = unknown> {
184
+ /** Map from serialized value key to tagged entry. */
185
+ private _entries;
186
+ private readonly _clock;
187
+ private readonly _documentId;
188
+ private readonly _field;
189
+ private readonly _pendingOps;
190
+ private _listeners;
191
+ constructor(documentId: string, field: string, clock: HybridLogicalClock);
192
+ get values(): T[];
193
+ get size(): number;
194
+ has(item: T): boolean;
195
+ add(item: T): Operation;
196
+ remove(item: T): Operation;
197
+ /** Apply a remote operation. */
198
+ applyRemote(op: Operation): void;
199
+ drainOps(): Operation[];
200
+ onChange(callback: SetChangeCallback<T>): () => void;
201
+ private _keyOf;
202
+ private _notifyChange;
203
+ }
204
+
205
+ /**
206
+ * CRDTRegister<T> -- Last-Writer-Wins Register (LWW-Register).
207
+ *
208
+ * The value with the highest HLC timestamp wins.
209
+ * Ties are broken by site id comparison.
210
+ */
211
+
212
+ type RegisterChangeCallback<T> = (value: T | undefined) => void;
213
+ declare class CRDTRegister<T = unknown> {
214
+ private _value;
215
+ private _hlc;
216
+ private readonly _clock;
217
+ private readonly _documentId;
218
+ private readonly _field;
219
+ private readonly _pendingOps;
220
+ private _listeners;
221
+ constructor(documentId: string, field: string, clock: HybridLogicalClock);
222
+ get value(): T | undefined;
223
+ set(value: T): Operation;
224
+ /** Apply a remote set operation. LWW: only apply if remote HLC > current. */
225
+ applyRemote(op: Operation): void;
226
+ /** Export current state for full-state sync. */
227
+ exportState(): {
228
+ value: T | undefined;
229
+ hlc: HLCTimestamp | null;
230
+ };
231
+ /** Import state from full-state sync. LWW merge. */
232
+ importState(value: T, hlc: HLCTimestamp): void;
233
+ drainOps(): Operation[];
234
+ onChange(callback: RegisterChangeCallback<T>): () => void;
235
+ private _notifyChange;
236
+ }
237
+
238
+ /**
239
+ * CRDTDocument -- container for collaborative fields.
240
+ *
241
+ * A document holds named CRDT fields (text, counter, set, register) that
242
+ * share a single HLC clock and sync channel. Operations from all fields
243
+ * are collected and sent to the server together.
244
+ */
245
+
246
+ type DocumentChangeCallback = (ops: Operation[]) => void;
247
+ declare class CRDTDocument {
248
+ readonly id: string;
249
+ readonly clock: HybridLogicalClock;
250
+ private _texts;
251
+ private _counters;
252
+ private _sets;
253
+ private _registers;
254
+ private _listeners;
255
+ /** Buffer for ops not yet synced. */
256
+ private _unsyncedOps;
257
+ constructor(id: string, siteId?: string);
258
+ getText(field: string): CRDTText;
259
+ getCounter(field: string): CRDTCounter;
260
+ getSet<T = unknown>(field: string): CRDTSet<T>;
261
+ getRegister<T = unknown>(field: string): CRDTRegister<T>;
262
+ /**
263
+ * Collect all pending local operations from all fields.
264
+ * Drains the per-field op buffers and returns them.
265
+ */
266
+ collectOps(): Operation[];
267
+ /**
268
+ * Acknowledge that operations up to and including `opId` have been
269
+ * persisted by the server. Removes them from the unsynced buffer.
270
+ */
271
+ acknowledge(opId: string): void;
272
+ /** Get operations that have not been acknowledged by the server. */
273
+ getUnsyncedOps(): readonly Operation[];
274
+ /**
275
+ * Apply a batch of remote operations to the appropriate CRDT fields.
276
+ */
277
+ applyRemoteOps(ops: Operation[]): void;
278
+ private _applyRemoteOp;
279
+ /**
280
+ * Encode all pending operations as a Uint8Array (JSON wire format).
281
+ * Used for WebSocket transmission.
282
+ */
283
+ encode(): Uint8Array;
284
+ /**
285
+ * Decode and apply a remote message (Uint8Array or string).
286
+ */
287
+ decode(data: Uint8Array | string): void;
288
+ onChange(callback: DocumentChangeCallback): () => void;
289
+ private _notifyChange;
290
+ }
291
+
292
+ /**
293
+ * CRDTSync -- WebSocket-based sync protocol handler.
294
+ *
295
+ * Connects to the Base server's CRDT sync endpoint and manages
296
+ * bidirectional operation exchange. Handles:
297
+ *
298
+ * - Buffering and batching local operations
299
+ * - Applying remote operations to the CRDTDocument
300
+ * - Reconnection with exponential backoff
301
+ * - Acknowledgment tracking
302
+ * - Presence (peer awareness) via the same channel
303
+ */
304
+
305
+ type SyncState = 'disconnected' | 'connecting' | 'connected' | 'syncing';
306
+ interface PeerState {
307
+ siteId: string;
308
+ /** Arbitrary JSON-serializable state (cursor position, name, color, etc.). */
309
+ meta: Record<string, unknown>;
310
+ /** Last seen timestamp (local clock). */
311
+ lastSeen: number;
312
+ }
313
+ type SyncStateCallback = (state: SyncState) => void;
314
+ type PeerCallback = (peers: Map<string, PeerState>) => void;
315
+ type RemoteChangeCallback = (ops: Operation[]) => void;
316
+ declare class CRDTSync {
317
+ private _ws;
318
+ private _state;
319
+ private _document;
320
+ private _wsUrl;
321
+ /** Flush interval for batching local ops. */
322
+ private _flushTimer;
323
+ private _flushIntervalMs;
324
+ /** Reconnection. */
325
+ private _reconnectTimer;
326
+ private _reconnectAttempts;
327
+ private _maxReconnectDelay;
328
+ private _baseReconnectDelay;
329
+ private _intentionalClose;
330
+ /** Presence. */
331
+ private _peers;
332
+ private _localPresence;
333
+ private _presenceTimer;
334
+ private _presenceIntervalMs;
335
+ private _peerTimeoutMs;
336
+ /** Listeners. */
337
+ private _stateListeners;
338
+ private _peerListeners;
339
+ private _remoteChangeListeners;
340
+ get state(): SyncState;
341
+ get peers(): ReadonlyMap<string, PeerState>;
342
+ /**
343
+ * Connect to the CRDT sync endpoint and start syncing a document.
344
+ *
345
+ * @param wsUrl - WebSocket URL, e.g. "wss://myapp.hanzo.ai/api/crdt"
346
+ * @param document - The CRDTDocument to sync.
347
+ * @param token - Optional auth token.
348
+ */
349
+ connect(wsUrl: string, document: CRDTDocument, token?: string): void;
350
+ disconnect(): void;
351
+ /** Update local presence metadata (cursor, name, etc.). */
352
+ updatePresence(meta: Record<string, unknown>): void;
353
+ onStateChange(cb: SyncStateCallback): () => void;
354
+ onPeersChange(cb: PeerCallback): () => void;
355
+ onRemoteChange(cb: RemoteChangeCallback): () => void;
356
+ private _handleMessage;
357
+ private _startFlush;
358
+ private _stopFlush;
359
+ private _flush;
360
+ private _sendOps;
361
+ private _startPresence;
362
+ private _stopPresence;
363
+ private _broadcastPresence;
364
+ private _pruneStalePresence;
365
+ private _scheduleReconnect;
366
+ private _clearReconnect;
367
+ private _send;
368
+ private _setState;
369
+ private _notifyPeers;
370
+ }
371
+
372
+ export { CRDTCounter, CRDTDocument, CRDTRegister, CRDTSet, CRDTSync, CRDTText, type CounterChangeCallback, type CounterIncrementPayload, type DocumentChangeCallback, type HLCTimestamp, HybridLogicalClock, type Operation, type OperationPayload, type OperationType, type PeerCallback, type PeerState, type RegisterChangeCallback, type RegisterSetPayload, type RemoteChangeCallback, type SetAddPayload, type SetChangeCallback, type SetRemovePayload, type SyncState, type SyncStateCallback, type TextChangeCallback, type TextDeletePayload, type TextInsertPayload, compareHLC, makeOpId, makePositionId };
@@ -0,0 +1,3 @@
1
+ export { CRDTCounter, CRDTDocument, CRDTRegister, CRDTSet, CRDTSync, CRDTText, HybridLogicalClock, compareHLC, makeOpId, makePositionId } from '../chunk-5NHFZRMO.js';
2
+ //# sourceMappingURL=index.js.map
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"index.js"}
@@ -0,0 +1,144 @@
1
+ import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { ReactNode } from 'react';
3
+ import { AuthStore, BaseClient, BaseRecord, ListOptions, ConnectionState, RealtimeEvent } from '../core/index.js';
4
+ export { BaseClientError, ClientConfig, ListResult, StateVersion } from '../core/index.js';
5
+ import { CRDTDocument, CRDTText, CRDTCounter, CRDTSet, CRDTRegister, CRDTSync, SyncState, PeerState } from '../crdt/index.js';
6
+
7
+ interface BaseProviderProps {
8
+ /** Base URL for automatic client creation. */
9
+ url?: string;
10
+ /** Optional auth store override. */
11
+ authStore?: AuthStore;
12
+ /** Pre-created BaseClient (takes precedence over url). */
13
+ client?: BaseClient;
14
+ children: ReactNode;
15
+ }
16
+ declare function BaseProvider({ url, authStore, client: externalClient, children }: BaseProviderProps): react_jsx_runtime.JSX.Element;
17
+ /**
18
+ * Get the BaseClient from the nearest BaseProvider.
19
+ * Throws if used outside a provider.
20
+ */
21
+ declare function useBase(): BaseClient;
22
+
23
+ /**
24
+ * React hooks for Hanzo Base.
25
+ *
26
+ * All hooks use the BaseClient from the nearest BaseProvider context.
27
+ * They manage subscriptions, cleanup, and state transitions correctly
28
+ * with React 19 patterns (useEffect, useState, useCallback, useRef).
29
+ */
30
+
31
+ interface UseQueryOptions extends ListOptions {
32
+ /** Whether to auto-subscribe to realtime updates. Default: true. */
33
+ realtime?: boolean;
34
+ /** Whether to run the query immediately. Default: true. */
35
+ enabled?: boolean;
36
+ }
37
+ interface UseQueryResult<T = BaseRecord> {
38
+ data: T[];
39
+ isLoading: boolean;
40
+ error: Error | null;
41
+ /** Manually refetch the query. */
42
+ refetch: () => Promise<void>;
43
+ }
44
+ /**
45
+ * Reactive query hook.
46
+ *
47
+ * Fetches records from a collection and subscribes to realtime updates.
48
+ * Results are cached in the QueryStore for deduplication across components.
49
+ */
50
+ declare function useQuery<T extends BaseRecord = BaseRecord>(collection: string, options?: UseQueryOptions): UseQueryResult<T>;
51
+ type MutationAction = 'create' | 'update' | 'delete';
52
+ interface MutateOptions {
53
+ /** If true, apply an optimistic update to the QueryStore before the server responds. */
54
+ optimistic?: boolean;
55
+ }
56
+ interface UseMutationResult {
57
+ mutate: (data: Record<string, unknown>, options?: MutateOptions) => Promise<BaseRecord | void>;
58
+ isLoading: boolean;
59
+ error: Error | null;
60
+ }
61
+ /**
62
+ * Mutation hook.
63
+ *
64
+ * Performs a create, update, or delete mutation against a collection.
65
+ * Supports optimistic updates that are automatically rolled back on failure.
66
+ */
67
+ declare function useMutation(collection: string, action: MutationAction): UseMutationResult;
68
+ interface UseAuthResult {
69
+ user: BaseRecord | null;
70
+ token: string;
71
+ isValid: boolean;
72
+ signIn: (identity: string, password: string) => Promise<void>;
73
+ signUp: (data: Record<string, unknown>) => Promise<BaseRecord>;
74
+ signOut: () => void;
75
+ /** Subscribe to auth changes. Returns unsubscribe. */
76
+ onChange: AuthStore['onChange'];
77
+ }
78
+ /**
79
+ * Auth state hook.
80
+ *
81
+ * Returns the current auth state and provides sign-in, sign-up, and
82
+ * sign-out methods. Re-renders when auth state changes.
83
+ *
84
+ * @param collection - Auth collection name (default: "users").
85
+ */
86
+ declare function useAuth(collection?: string): UseAuthResult;
87
+ /**
88
+ * Low-level realtime subscription hook.
89
+ *
90
+ * Subscribes to SSE events for a collection topic on mount and
91
+ * unsubscribes on unmount.
92
+ */
93
+ declare function useRealtime(collection: string, topic: string, callback: (event: RealtimeEvent) => void): void;
94
+ declare function useConnectionState(): ConnectionState;
95
+ interface UsePresenceResult {
96
+ /** Map of peer siteId to their state. */
97
+ peers: Map<string, PeerState>;
98
+ /** Our own presence metadata. */
99
+ myState: Record<string, unknown>;
100
+ /** Update our presence metadata. */
101
+ updateState: (meta: Record<string, unknown>) => void;
102
+ }
103
+ /**
104
+ * Presence tracking hook.
105
+ *
106
+ * Requires a CRDTSync instance (usually obtained from useCRDT).
107
+ * Tracks peers connected to the same document.
108
+ */
109
+ declare function usePresence(sync: CRDTSync): UsePresenceResult;
110
+ interface UseCRDTResult {
111
+ /** The CRDTDocument instance. */
112
+ doc: CRDTDocument;
113
+ /** Convenience accessor for text fields. */
114
+ text: (field: string) => CRDTText;
115
+ /** Convenience accessor for counter fields. */
116
+ counter: (field: string) => CRDTCounter;
117
+ /** Convenience accessor for set fields. */
118
+ set: <T = unknown>(field: string) => CRDTSet<T>;
119
+ /** Convenience accessor for register fields. */
120
+ register: <T = unknown>(field: string) => CRDTRegister<T>;
121
+ /** The CRDTSync instance for advanced usage (presence, state listeners). */
122
+ sync: CRDTSync;
123
+ /** Current sync state. */
124
+ syncState: SyncState;
125
+ }
126
+ /**
127
+ * CRDT document hook.
128
+ *
129
+ * Creates a CRDTDocument and CRDTSync, connects to the server via
130
+ * WebSocket, and auto-syncs all local operations.
131
+ *
132
+ * @param documentId - Unique document identifier.
133
+ * @param wsUrl - WebSocket URL for the CRDT sync endpoint. If not provided,
134
+ * derives it from the BaseClient URL (http->ws, https->wss).
135
+ */
136
+ declare function useCRDT(documentId: string, wsUrl?: string): UseCRDTResult;
137
+ /**
138
+ * Subscribe to a specific CRDTText field's current string value.
139
+ * Re-renders whenever the text changes (local or remote).
140
+ */
141
+ declare function useCRDTText(doc: CRDTDocument, field: string): string;
142
+ declare function useCRDTCounter(doc: CRDTDocument, field: string): number;
143
+
144
+ export { AuthStore, BaseClient, BaseProvider, type BaseProviderProps, BaseRecord, ConnectionState, ListOptions, type MutateOptions, type MutationAction, RealtimeEvent, type UseAuthResult, type UseCRDTResult, type UseMutationResult, type UsePresenceResult, type UseQueryOptions, type UseQueryResult, useAuth, useBase, useCRDT, useCRDTCounter, useCRDTText, useConnectionState, useMutation, usePresence, useQuery, useRealtime };