@fitzzero/quickdraw-core 3.9.0 → 4.0.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/README.md CHANGED
@@ -5,6 +5,7 @@ Fast fullstack patterns for real-time applications with Socket.io and TanStack Q
5
5
  ## Features
6
6
 
7
7
  - **Server Core**: BaseService class with typed CRUD, ACL-based access control, and real-time subscriptions
8
+ - **Collections**: declare a scope-keyed list once, get live add/update/remove deltas, pagination, and reconnect healing — no hand-wired events
8
9
  - **Client Core**: TanStack Query integration with Socket.io for real-time state management
9
10
  - **Socket Inputs**: Pre-built form components that sync with server state
10
11
  - **Custom OAuth**: JWT-based authentication with Discord and Google providers
@@ -110,6 +111,88 @@ function ChatPage({ chatId }: { chatId: string }) {
110
111
  }
111
112
  ```
112
113
 
114
+ ### Collections (Live Lists)
115
+
116
+ Entity subscriptions cover single rows; **collections** cover lists. A
117
+ collection is "rows of this service, grouped by a scope id derived from the
118
+ row" — declare it once and the framework handles emission (multi-node-safe),
119
+ pagination, and reconnect correctness. No more `task:created` /
120
+ `task:deleted` mirror events, `invalidateOn` refetches, or hand-rolled
121
+ merge-by-id state.
122
+
123
+ **Server** — declare next to your methods; the CRUD trio emits deltas
124
+ automatically (scope moves and predicate entry/exit included):
125
+
126
+ ```typescript
127
+ type MessageCollections = { byChat: { item: MessageDTO } };
128
+
129
+ class MessageService extends BaseService<
130
+ Message,
131
+ Prisma.MessageCreateInput,
132
+ Prisma.MessageUpdateInput,
133
+ MessageServiceMethods,
134
+ Record<string, unknown>, // channels
135
+ MessageDTO, // TDto — wire shape
136
+ MessageCollections // TCollections
137
+ > {
138
+ constructor(prisma: PrismaClient) {
139
+ super({ serviceName: "messageService" });
140
+ this.setDelegate(prisma.message);
141
+
142
+ this.defineCollection("byChat", {
143
+ resolveScopeId: (message) => message.chatId,
144
+ checkScopeAccess: (userId, chatId) => this.isChatMember(userId, chatId),
145
+ // Server-ordered first page + reconnect re-snapshot. Omit `ids` for
146
+ // unbounded histories like this one; return it for bounded scopes so
147
+ // reconnecting clients prune rows deleted while offline.
148
+ snapshot: async (chatId, { cursor, limit }) => this.getMessagePage(chatId, cursor, limit),
149
+ toItem: (message) => this.toDto(message),
150
+ });
151
+ }
152
+ }
153
+ ```
154
+
155
+ **Client** — one hook per list; live deltas, `loadMore` paging, and
156
+ re-snapshot-on-reconnect are built in:
157
+
158
+ ```tsx
159
+ import { useCollection } from "@fitzzero/quickdraw-core/client";
160
+
161
+ function ChatWindow({ chatId }: { chatId: string }) {
162
+ const {
163
+ items: messages,
164
+ isLoading,
165
+ hasMore,
166
+ loadMore,
167
+ } = useCollection<MessageDTO>("messageService", "byChat", chatId, {
168
+ compare: (a, b) => a.createdAt.localeCompare(b.createdAt),
169
+ });
170
+
171
+ return <MessageList messages={messages} onScrollTop={hasMore ? loadMore : undefined} />;
172
+ }
173
+ ```
174
+
175
+ Scopes don't have to be parent entities — `resolveScopeId` may return a
176
+ `string[]` to fan out (e.g. a chat appearing in every member's `myChats`
177
+ collection, scope = user id), or `null` to exclude a row (predicate
178
+ filtering). For hand-rolled write paths, one-line choke points keep deltas
179
+ flowing: `emitCollectionUpsert` / `emitCollectionRemove` /
180
+ `emitCollectionMove` / `emitCollectionReset`, plus `kickFromCollection` for
181
+ adapter-safe ACL revocation.
182
+
183
+ ACL is deliberately simple: items are **scope-visible** — anyone who passes
184
+ `checkScopeAccess` sees every item in full (strip sensitive fields in
185
+ `toItem`/`snapshot`). If visibility varies per user within a scope, that's
186
+ not a collection — use separate scopes or entity subscriptions.
187
+
188
+ When to use which:
189
+
190
+ | Hook | Use for |
191
+ | ----------------- | ------------------------------------------------------------- |
192
+ | `useSubscription` | One entity, field-tiered (detail panels) |
193
+ | `useCollection` | Live lists of a scope (boards, feeds, chat histories) |
194
+ | `useServiceQuery` | Genuinely query-shaped reads (search, cross-scope aggregates) |
195
+
113
196
  ### Socket Inputs
114
197
 
115
198
  ```tsx
@@ -131,54 +214,71 @@ function ChatTitleEditor({ chat, updateChat }) {
131
214
 
132
215
  ### Custom Room Events
133
216
 
134
- When your server broadcasts custom events to subscription rooms via `emitToRoom`, use `useRoomEvents` on the client to listen with proper lifecycle management:
217
+ For genuinely custom, ephemeral events (typing indicators, presence pulses —
218
+ things that aren't rows), broadcast with `emitToRoom` and listen with
219
+ `useRoomEvents`:
135
220
 
136
221
  ```tsx
137
222
  import { useSubscription, useRoomEvents } from "@fitzzero/quickdraw-core/client";
138
223
 
139
- function ProjectBoard({ projectId }: { projectId: string }) {
140
- const { data: project } = useSubscription("projectService", projectId);
141
- const [tasks, setTasks] = useState<Task[]>([]);
224
+ function ChatView({ chatId }: { chatId: string }) {
225
+ const { data: chat } = useSubscription("chatService", chatId);
226
+ const [typing, setTyping] = useState<string | null>(null);
142
227
 
143
228
  // Lifecycle-managed event listeners — cleanup handled automatically
144
229
  useRoomEvents({
145
- "task:created": (task: Task) => setTasks((prev) => [...prev, task]),
146
- "task:deleted": ({ id }: { id: string }) => setTasks((prev) => prev.filter((t) => t.id !== id)),
230
+ "chat:typing": ({ userName }: { userName: string }) => setTyping(userName),
231
+ "chat:typingStop": () => setTyping(null),
147
232
  });
148
233
 
149
- return <Board tasks={tasks} />;
234
+ return <Chat chat={chat} typing={typing} />;
235
+ }
236
+ ```
237
+
238
+ Event names and payloads can be typed end-to-end by augmenting
239
+ `QuickdrawEventMap` from the package root — `emitToRoom` and `useRoomEvents`
240
+ then check payloads and autocomplete names (and degrade to
241
+ `string`/`unknown` if you never augment it):
242
+
243
+ ```typescript
244
+ declare module "@fitzzero/quickdraw-core" {
245
+ interface QuickdrawEventMap {
246
+ "chat:typing": { userName: string };
247
+ "chat:typingStop": Record<string, never>;
248
+ }
150
249
  }
151
250
  ```
152
251
 
252
+ Don't hand-emit row lifecycle events (`task:created`, `task:deleted`, …) —
253
+ that's what collections automate; the shipped
254
+ `quickdraw/no-manual-collection-events` lint rule flags them.
255
+
153
256
  ### Auto-Invalidating Queries
154
257
 
155
- For list queries that should refresh when related events fire, use `invalidateOn`:
258
+ For genuinely query-shaped reads (search results, cross-scope aggregates)
259
+ that should refresh when related events fire, use `invalidateOn`:
156
260
 
157
261
  ```tsx
158
262
  import { useServiceQuery } from "@fitzzero/quickdraw-core/client";
159
263
 
160
- function TaskList({ projectId }: { projectId: string }) {
161
- // Auto-refetches when tasks are created, deleted, or change status
162
- const { data: tasks } = useServiceQuery(
264
+ function SearchResults({ query }: { query: string }) {
265
+ const { data: results } = useServiceQuery(
163
266
  "taskService",
164
- "listTasks",
165
- { projectId },
267
+ "searchTasks",
268
+ { query },
166
269
  {
167
- invalidateOn: ["task:created", "task:deleted", "task:statusUpdate"],
270
+ invalidateOn: ["task:statusUpdate"],
271
+ refetchInterval: 60_000, // optional periodic refresh
168
272
  },
169
273
  );
170
274
 
171
- return (
172
- <ul>
173
- {tasks?.map((t) => (
174
- <li key={t.id}>{t.title}</li>
175
- ))}
176
- </ul>
177
- );
275
+ return <Results items={results} />;
178
276
  }
179
277
  ```
180
278
 
181
- Rapid-fire events within 100ms are debounced into a single refetch.
279
+ Rapid-fire events within 100ms are debounced into a single refetch. For
280
+ plain scope lists, prefer `useCollection` — it replaces the
281
+ `invalidateOn` + refetch cycle with true deltas.
182
282
 
183
283
  ### Channels (High-Frequency Traffic)
184
284
 
@@ -285,17 +385,89 @@ When to use which:
285
385
  | ACL | full async check incl. DB | in-memory only |
286
386
  | Rate limit | global limiter | per-channel token bucket |
287
387
 
388
+ ## Splitting Large Services
389
+
390
+ Real services grow past what one file should hold. The proven pattern —
391
+ battle-tested in the framework's largest consumer without import cycles — is
392
+ an abstract `*ServiceCore` plus method modules wired by a thin concrete
393
+ subclass:
394
+
395
+ ```typescript
396
+ // services/task/service-core.ts — state, ACL overrides, helpers. No methods.
397
+ export abstract class TaskServiceCore extends BaseService<
398
+ Task,
399
+ Prisma.TaskCreateInput,
400
+ Prisma.TaskUpdateInput,
401
+ TaskServiceMethods,
402
+ TaskChannels,
403
+ TaskDTO,
404
+ TaskCollections
405
+ > {
406
+ constructor(protected readonly prisma: PrismaClient) {
407
+ super({ serviceName: "taskService" });
408
+ this.setDelegate(prisma.task);
409
+ }
410
+
411
+ public buildCardDTO(taskId: string): Promise<TaskCardDTO> {
412
+ /* ... */
413
+ }
414
+ }
415
+
416
+ // services/task/methods/create-task.ts — one module per method (or cluster).
417
+ // defineMethod is public precisely so modules can register on the instance.
418
+ export function registerCreateTask(service: TaskService): void {
419
+ service.defineMethod(
420
+ "createTask",
421
+ "Read",
422
+ async (payload, ctx) => {
423
+ // ...
424
+ },
425
+ { schema: createTaskSchema },
426
+ );
427
+ }
428
+
429
+ // services/task/index.ts — the concrete subclass wires the modules.
430
+ export class TaskService extends TaskServiceCore {
431
+ constructor(prisma: PrismaClient) {
432
+ super(prisma);
433
+ registerCreateTask(this);
434
+ registerUpdateTask(this);
435
+ // ...
436
+ this.verifyAllMethods(["createTask", "updateTask" /* ... */]);
437
+ }
438
+ }
439
+ ```
440
+
441
+ Core → modules → concrete class is a DAG: the core never imports the modules,
442
+ the modules never import each other. `verifyAllMethods` catches a forgotten
443
+ `register*` call at boot. The public choke points (`emitUpdate`,
444
+ `emitCollectionUpsert`, `emitToRoom`, `isLevelSufficient`, …) exist so method
445
+ modules outside the class stay fully capable.
446
+
288
447
  ## Package Exports
289
448
 
290
449
  ```typescript
291
450
  // Shared types (both server and client)
292
- import { ServiceResponse, AccessLevel, ServiceMethodMap } from "@fitzzero/quickdraw-core";
451
+ import {
452
+ ServiceResponse,
453
+ AccessLevel,
454
+ ServiceMethodMap,
455
+ // Room helpers + typed events (4.0)
456
+ serviceRoom,
457
+ collectionRoom,
458
+ userRoom,
459
+ type QuickdrawEventMap,
460
+ type CollectionDelta,
461
+ } from "@fitzzero/quickdraw-core";
293
462
 
294
463
  // Server
295
464
  import {
296
465
  BaseService,
466
+ BaseRpcService, // 4.0: method-only services, no delegate/CRUD
297
467
  ServiceRegistry,
298
468
  createQuickdrawServer,
469
+ type CollectionDefinition, // 4.0
470
+ type QuickdrawIdentity, // 4.0: structured authenticate result
299
471
  createJWT,
300
472
  verifyJWT,
301
473
  discordProvider,
@@ -340,7 +512,9 @@ import {
340
512
  useService,
341
513
  useServiceQuery,
342
514
  useSubscription,
515
+ useCollection, // 4.0: live scope-keyed lists
343
516
  useRoomEvents,
517
+ ServiceCallError, // 4.0: hook errors carry the server code
344
518
  SocketCheckbox,
345
519
  SocketTextField,
346
520
  SocketSelect,
@@ -436,6 +610,7 @@ export type ChatServiceMethods = ServiceMethodMap<{
436
610
  │ │
437
611
  │ useService() ──────────────────────────────────────────────┐ │
438
612
  │ useSubscription() ─────────────────────────────────────────┤ │
613
+ │ useCollection() ───────────────────────────────────────────┤ │
439
614
  │ SocketTextField, SocketCheckbox, ... ──────────────────────┤ │
440
615
  │ │ │
441
616
  └─────────────────────────────────────────────────────────────│───┘
@@ -450,10 +625,11 @@ export type ChatServiceMethods = ServiceMethodMap<{
450
625
  │ ├── Auto-discovers public methods │
451
626
  │ └── Wires methods to Socket.io events │
452
627
  │ │
453
- │ BaseService<Entity, CreateInput, UpdateInput, ServiceMethods> │
628
+ │ BaseService<Entity, Create, Update, Methods, …, Dto, Colls> │
454
629
  │ ├── defineMethod() - Type-safe method definition │
630
+ │ ├── defineCollection() - Live lists with automatic deltas │
455
631
  │ ├── subscribe() / unsubscribe() - Real-time subscriptions │
456
- │ ├── create() / update() / delete() - CRUD with auto-emit │
632
+ │ ├── create() / update() / delete() - CRUD, auto-emit + hooks │
457
633
  │ └── checkAccess() - ACL enforcement │
458
634
  │ │
459
635
  │ Auth Utilities │
@@ -1,9 +1,9 @@
1
1
  import * as React from 'react';
2
- import { a as QuickdrawProviderProps, Q as QuickdrawSocketContextValue, U as UseServiceOptions, b as UseServiceResult, c as UseServiceQueryOptions, d as UseServiceQueryResult, e as UseSubscriptionOptions, f as UseSubscriptionResult, g as UseRoomEventsOptions, h as UseChannelSendResult } from '../types-DUHPIy_5.js';
3
- export { C as ClientServiceMethodMap, S as SubscriptionDataMap } from '../types-DUHPIy_5.js';
2
+ import { a as QuickdrawProviderProps, Q as QuickdrawSocketContextValue, U as UseServiceOptions, b as UseServiceResult, c as UseServiceQueryOptions, d as UseServiceQueryResult, e as UseSubscriptionOptions, f as UseSubscriptionResult, g as UseCollectionOptions, h as UseCollectionResult, i as QuickdrawRoomEventHandlers, j as UseRoomEventsOptions, k as UseChannelSendResult } from '../types-CX5LuC_8.js';
3
+ export { C as ClientServiceMethodMap, S as SubscriptionDataMap } from '../types-CX5LuC_8.js';
4
+ import { CollectionSnapshotResponse, CollectionDelta } from '../shared/index.js';
4
5
  import 'socket.io-client';
5
6
  import '@tanstack/react-query';
6
- import '../shared/index.js';
7
7
  import 'zod';
8
8
 
9
9
  /**
@@ -28,7 +28,7 @@ declare function useQuickdrawSocket(): QuickdrawSocketContextValue;
28
28
  * }
29
29
  * ```
30
30
  */
31
- declare function QuickdrawProvider({ children, serverUrl, queryClient, authToken, autoConnect, withCredentials, socketPath, }: QuickdrawProviderProps): React.ReactElement;
31
+ declare function QuickdrawProvider({ children, serverUrl, queryClient, authToken, autoConnect, withCredentials, socketPath, reconnectBehavior, }: QuickdrawProviderProps): React.ReactElement;
32
32
 
33
33
  /**
34
34
  * Hook for invoking service methods via Socket.io with TanStack Query integration.
@@ -171,6 +171,37 @@ declare function useSubscription<TData extends {
171
171
  id: string;
172
172
  }>(serviceName: string, entryId: string | null, options?: UseSubscriptionOptions<TData>): UseSubscriptionResult<TData>;
173
173
 
174
+ /**
175
+ * Hook for subscribing to a server-declared collection: a scope-keyed,
176
+ * live-updating list (`defineCollection` server-side).
177
+ *
178
+ * The initial snapshot arrives via `{service}:collection:subscribe`;
179
+ * `added`/`updated`/`removed` deltas stream to the scope room and merge
180
+ * id-keyed with per-item last-writer-wins; `reset` deltas trigger a
181
+ * debounced re-snapshot. Reconnects re-snapshot automatically (the same
182
+ * registry cycle entity subscriptions use), pruning rows deleted while
183
+ * offline when the server provides membership `ids`.
184
+ *
185
+ * Subscriptions are deduped across components by scope; pagination reuses
186
+ * the subscribe event with a cursor via `loadMore`.
187
+ *
188
+ * @example
189
+ * ```tsx
190
+ * function ChatWindow({ chatId }: { chatId: string }) {
191
+ * const { items: messages, loadMore, hasMore } = useCollection<MessageDTO>(
192
+ * "messageService",
193
+ * "byChat",
194
+ * chatId,
195
+ * { compare: (a, b) => a.createdAt.localeCompare(b.createdAt) },
196
+ * );
197
+ * // messages update live; loadMore() pages in older history
198
+ * }
199
+ * ```
200
+ */
201
+ declare function useCollection<TItem extends {
202
+ id: string;
203
+ }>(serviceName: string, collection: string, scopeId: string | null, options?: UseCollectionOptions<TItem>): UseCollectionResult<TItem>;
204
+
174
205
  /**
175
206
  * Hook for listening to custom socket events with managed lifecycle.
176
207
  *
@@ -178,6 +209,11 @@ declare function useSubscription<TData extends {
178
209
  * and connection-awareness. Each component gets its own listeners (no dedup needed).
179
210
  * Room scoping is handled server-side via `emitToRoom`.
180
211
  *
212
+ * Event names and payloads are typed via the augmentable `QuickdrawEventMap`
213
+ * (from the package root): augmented names get payload checking and
214
+ * autocomplete; unlisted names still work, untyped — nothing breaks if the
215
+ * map stays empty.
216
+ *
181
217
  * Pair with `useSubscription` to ensure the socket is in the correct room:
182
218
  * `useSubscription` manages room membership, `useRoomEvents` manages event listeners.
183
219
  *
@@ -205,7 +241,7 @@ declare function useSubscription<TData extends {
205
241
  * }
206
242
  * ```
207
243
  */
208
- declare function useRoomEvents(handlers: Record<string, (data: never) => void>, options?: UseRoomEventsOptions): void;
244
+ declare function useRoomEvents(handlers: QuickdrawRoomEventHandlers, options?: UseRoomEventsOptions): void;
209
245
 
210
246
  /**
211
247
  * Hook for sending fire-and-forget channel messages (the high-frequency
@@ -237,6 +273,96 @@ declare function useRoomEvents(handlers: Record<string, (data: never) => void>,
237
273
  */
238
274
  declare function useChannelSend<TPayload = unknown>(serviceName: string, channelName: string): UseChannelSendResult<TPayload>;
239
275
 
276
+ /**
277
+ * Pure merge logic for collection subscriptions — the correctness heart of
278
+ * `useCollection`, kept free of React/socket dependencies so every rule is
279
+ * unit-testable.
280
+ *
281
+ * Invariants:
282
+ * - Everything is id-keyed; live deltas and paged history compose.
283
+ * - Per-item last-writer-wins by `rev` (epoch ms). A delta older than what
284
+ * the cache holds for that id is ignored; snapshot/page items only
285
+ * overwrite cached items whose rev predates the response's rev.
286
+ * - Removals leave a tombstone rev in `revById` so a stale `added` cannot
287
+ * resurrect a deleted row; snapshots garbage-collect stale tombstones.
288
+ */
289
+ interface CollectionCacheEntry<TItem extends {
290
+ id: string;
291
+ }> {
292
+ byId: Record<string, TItem>;
293
+ /** Insertion/server order; render order applies `compare` on top. */
294
+ order: string[];
295
+ /** Last-known rev per id — includes tombstones for removed ids. */
296
+ revById: Record<string, number>;
297
+ nextCursor: string | null;
298
+ totalCount: number | null;
299
+ /** Rev of the last cursor-less snapshot applied. */
300
+ snapshotRev: number;
301
+ }
302
+ interface ApplyDeltaOptions {
303
+ /** Where `added` lands when the id is new. Default "end". */
304
+ insertPosition?: "start" | "end";
305
+ }
306
+ interface ApplyDeltaResult<TItem extends {
307
+ id: string;
308
+ }> {
309
+ entry: CollectionCacheEntry<TItem>;
310
+ /** True when the delta was a `reset` — caller schedules a re-snapshot. */
311
+ resetRequested: boolean;
312
+ }
313
+ declare function createEmptyEntry<TItem extends {
314
+ id: string;
315
+ }>(): CollectionCacheEntry<TItem>;
316
+ /**
317
+ * Apply a cursor-less snapshot (initial load or re-snapshot).
318
+ *
319
+ * - When `ids` is present it is the authoritative membership: cached items
320
+ * absent from it are pruned — unless a live delta newer than the snapshot
321
+ * put them there (`revById[id] > snapshot.rev`), in which case the live
322
+ * push wins.
323
+ * - Page items upsert unless the cache holds a newer rev for that id.
324
+ * - Order: page order first (fresh server order), then surviving previously
325
+ * known ids in their prior relative order (paged-in history stays).
326
+ * - Tombstones older than the snapshot rev are garbage-collected; newer
327
+ * ones keep suppressing their id (the removal happened after the
328
+ * snapshot query ran).
329
+ */
330
+ declare function applySnapshot<TItem extends {
331
+ id: string;
332
+ }>(prev: CollectionCacheEntry<TItem> | null | undefined, snapshot: CollectionSnapshotResponse<TItem>): CollectionCacheEntry<TItem>;
333
+ /**
334
+ * Apply a cursored page (loadMore): upsert-only, never prune. Unseen ids
335
+ * append at the end in page order; existing ids keep their position (and
336
+ * their item, when the cache holds a newer rev).
337
+ */
338
+ declare function applyPage<TItem extends {
339
+ id: string;
340
+ }>(prev: CollectionCacheEntry<TItem> | null | undefined, page: CollectionSnapshotResponse<TItem>): CollectionCacheEntry<TItem>;
341
+ /**
342
+ * Apply a live delta. Returns the previous entry object unchanged when the
343
+ * delta is stale (rev LWW) so referential equality can skip re-renders.
344
+ */
345
+ declare function applyDelta<TItem extends {
346
+ id: string;
347
+ }>(prev: CollectionCacheEntry<TItem> | null | undefined, delta: CollectionDelta<TItem>, options?: ApplyDeltaOptions): ApplyDeltaResult<TItem>;
348
+ /**
349
+ * Apply a batch of buffered deltas in arrival order (used when deltas
350
+ * raced an in-flight snapshot). `resetRequested` is true if any was a reset.
351
+ */
352
+ declare function applyDeltas<TItem extends {
353
+ id: string;
354
+ }>(prev: CollectionCacheEntry<TItem> | null | undefined, deltas: CollectionDelta<TItem>[], options?: ApplyDeltaOptions): ApplyDeltaResult<TItem>;
355
+
356
+ /**
357
+ * Error thrown by client hooks when a service call acks with
358
+ * `{ success: false }`. Carries the server's optional numeric code
359
+ * (e.g. 401, 403, 429) so callers can branch without string matching.
360
+ */
361
+ declare class ServiceCallError extends Error {
362
+ readonly code?: number;
363
+ constructor(message: string, code?: number);
364
+ }
365
+
240
366
  type CommitMode = "change" | "blur" | "debounce";
241
367
  interface UseSocketInputOptions<TEntry, TAllowedUpdate, TKey extends keyof TAllowedUpdate> {
242
368
  /**
@@ -547,4 +673,4 @@ declare function buildBreadcrumbs(pathname: string, navigation: NavItem[]): Brea
547
673
  */
548
674
  declare function routeRequiresAuth(pathname: string, navigation: NavItem[], userMenuItems?: NavItem[]): boolean;
549
675
 
550
- export { type BreadcrumbItem, type CommitMode, type JWTPayload, type NavItem, QuickdrawProvider, QuickdrawProviderProps, QuickdrawSocketContextValue, SocketCheckbox, type SocketCheckboxProps, SocketSelect, type SocketSelectProps, SocketSlider, type SocketSliderProps, SocketSwitch, type SocketSwitchProps, SocketTextField, type SocketTextFieldProps, UseChannelSendResult, UseRoomEventsOptions, UseServiceOptions, UseServiceQueryOptions, UseServiceQueryResult, UseServiceResult, type UseSocketInputOptions, type UseSocketInputResult, UseSubscriptionOptions, UseSubscriptionResult, buildBreadcrumbs, clearAuthToken, findNavItemByHref, findParentNavItem, formatCurrency, formatDate, formatDateTime, formatNumber, formatPercent, getAuthToken, getOAuthUrl, logout, logoutAllDevices, parseJWTPayload, routeRequiresAuth, setAuthToken, truncate, useChannelSend, useQuickdrawSocket, useRoomEvents, useService, useServiceMethod, useServiceQuery, useSocketInput, useSubscription };
676
+ export { type BreadcrumbItem, type CollectionCacheEntry, type CommitMode, type JWTPayload, type NavItem, QuickdrawProvider, QuickdrawProviderProps, QuickdrawRoomEventHandlers, QuickdrawSocketContextValue, ServiceCallError, SocketCheckbox, type SocketCheckboxProps, SocketSelect, type SocketSelectProps, SocketSlider, type SocketSliderProps, SocketSwitch, type SocketSwitchProps, SocketTextField, type SocketTextFieldProps, UseChannelSendResult, UseCollectionOptions, UseCollectionResult, UseRoomEventsOptions, UseServiceOptions, UseServiceQueryOptions, UseServiceQueryResult, UseServiceResult, type UseSocketInputOptions, type UseSocketInputResult, UseSubscriptionOptions, UseSubscriptionResult, applyDelta, applyDeltas, applyPage, applySnapshot, buildBreadcrumbs, clearAuthToken, createEmptyEntry, findNavItemByHref, findParentNavItem, formatCurrency, formatDate, formatDateTime, formatNumber, formatPercent, getAuthToken, getOAuthUrl, logout, logoutAllDevices, parseJWTPayload, routeRequiresAuth, setAuthToken, truncate, useChannelSend, useCollection, useQuickdrawSocket, useRoomEvents, useService, useServiceMethod, useServiceQuery, useSocketInput, useSubscription };