@raisindb/client 0.2.4 → 0.2.5

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.
Files changed (44) hide show
  1. package/README.md +281 -70
  2. package/dist/chunk-3KMOXRTK.mjs +1377 -0
  3. package/dist/chunk-3KMOXRTK.mjs.map +1 -0
  4. package/dist/chunk-B6EFDYFS.mjs +170 -0
  5. package/dist/chunk-B6EFDYFS.mjs.map +1 -0
  6. package/dist/chunk-P44XWTPA.mjs +190 -0
  7. package/dist/chunk-P44XWTPA.mjs.map +1 -0
  8. package/dist/chunk-Q4IBPW2W.mjs +866 -0
  9. package/dist/chunk-Q4IBPW2W.mjs.map +1 -0
  10. package/dist/chunk-QJKYNLT4.mjs +532 -0
  11. package/dist/chunk-QJKYNLT4.mjs.map +1 -0
  12. package/dist/chunk-T2ZSNYFG.mjs +248 -0
  13. package/dist/chunk-T2ZSNYFG.mjs.map +1 -0
  14. package/dist/conversation-list-store-S5MPSqoB.d.mts +6823 -0
  15. package/dist/conversation-list-store-S5MPSqoB.d.ts +6823 -0
  16. package/dist/index-3-fS4i8b.d.mts +301 -0
  17. package/dist/index-Bs7UkpRs.d.ts +301 -0
  18. package/dist/index.d.mts +66 -6108
  19. package/dist/index.d.ts +66 -6108
  20. package/dist/index.js +1601 -201
  21. package/dist/index.js.map +1 -0
  22. package/dist/index.mjs +1199 -3046
  23. package/dist/index.mjs.map +1 -0
  24. package/dist/react.d.mts +4 -0
  25. package/dist/react.d.ts +4 -0
  26. package/dist/react.js +2519 -0
  27. package/dist/react.js.map +1 -0
  28. package/dist/react.mjs +9 -0
  29. package/dist/react.mjs.map +1 -0
  30. package/dist/svelte-sql-BCuXrg8L.d.mts +52 -0
  31. package/dist/svelte-sql-DO1X-WO3.d.ts +52 -0
  32. package/dist/svelte.d.mts +288 -0
  33. package/dist/svelte.d.ts +288 -0
  34. package/dist/svelte.js +2432 -0
  35. package/dist/svelte.js.map +1 -0
  36. package/dist/svelte.mjs +25 -0
  37. package/dist/svelte.mjs.map +1 -0
  38. package/dist/vue.d.mts +197 -0
  39. package/dist/vue.d.ts +197 -0
  40. package/dist/vue.js +1460 -0
  41. package/dist/vue.js.map +1 -0
  42. package/dist/vue.mjs +9 -0
  43. package/dist/vue.mjs.map +1 -0
  44. package/package.json +18 -3
package/README.md CHANGED
@@ -317,6 +317,99 @@ const subscription4 = await events.subscribeToNodeType('Page', (event) => {
317
317
  await subscription1.unsubscribe();
318
318
  ```
319
319
 
320
+ ### Build a chatbox
321
+
322
+ Everything chat lives behind `db.conversations` (low-level) and
323
+ `ConversationStore` (framework-agnostic state container). The store creates
324
+ the conversation lazily on the first message, streams the agent's reply over
325
+ SSE, tracks in-flight tool calls, and projects plans for approval UIs.
326
+
327
+ ```typescript
328
+ import { ConversationStore } from '@raisindb/client';
329
+
330
+ const db = client.database('myrepo');
331
+
332
+ const store = new ConversationStore({
333
+ database: db,
334
+ // Conversation is created on the first sendMessage(). The participant is
335
+ // an agent path, so this becomes an `ai_chat` conversation.
336
+ createOptions: { participant: '/agents/support' },
337
+ });
338
+
339
+ store.subscribe((s) => {
340
+ renderMessages(s.messages); // ChatMessage[]
341
+ if (s.isStreaming) renderPartial(s.streamingText); // live tokens
342
+
343
+ // Tool-call badges: "running" entries are in-flight
344
+ for (const tc of s.activeToolCalls) {
345
+ renderToolBadge(tc.functionName, tc.status); // running | completed | failed
346
+ }
347
+
348
+ // Plans awaiting approval
349
+ for (const plan of s.plans) {
350
+ if (plan.status === 'pending_approval') {
351
+ renderPlanCard(plan, {
352
+ approve: () => store.approvePlan(plan.planPath),
353
+ reject: (why) => store.rejectPlan(plan.planPath, why),
354
+ });
355
+ }
356
+ }
357
+
358
+ if (s.error) showError(s.error);
359
+ });
360
+
361
+ await store.sendMessage('Plan next week\'s shifts');
362
+ // ...
363
+ store.stop(); // abort the current turn in the UI
364
+ store.destroy(); // on teardown
365
+ ```
366
+
367
+ To resume an existing conversation, pass `conversationPath` instead of (or in
368
+ addition to) `createOptions` and call `store.loadMessages()` for history. To
369
+ build an inbox-style list of conversations, use `ConversationListStore`
370
+ (`load()`, `createConversation()`, `markAsRead()`, `realtime: true`).
371
+
372
+ ### Listen to the inbox (notifications without an extra API)
373
+
374
+ Server-side messaging delivers items into the logged-in user's home inbox in
375
+ the `raisin:access_control` workspace. A plain node subscription on
376
+ `${home}/inbox/**` is all an inbox bell needs — no polling, no extra API.
377
+
378
+ ```typescript
379
+ import { normalizeHomePath } from '@raisindb/client';
380
+
381
+ const user = await client.initSession('myrepo');
382
+
383
+ // user.home may be workspace-prefixed (/raisin:access_control/users/...).
384
+ // Subscription paths must be workspace-relative — always normalize.
385
+ const home = normalizeHomePath(user!.home)!;
386
+
387
+ await db.workspace('raisin:access_control').events().subscribe(
388
+ {
389
+ path: `${home}/inbox/**`,
390
+ event_types: ['node:created'],
391
+ include_node: true, // deliver the full node so we can render a title
392
+ },
393
+ (event) => {
394
+ const props = event.payload.node?.properties ?? {};
395
+ if (props.role === 'user') return; // skip the user's own chat messages
396
+ bumpBadge();
397
+ showToast((props.title as string) ?? (props.subject as string) ?? 'New inbox item');
398
+ },
399
+ );
400
+ ```
401
+
402
+ **Path filter semantics** (important — there is no implicit prefix matching):
403
+
404
+ | Pattern | Matches |
405
+ |---------|---------|
406
+ | `/users/u1/inbox` | exactly that node, nothing below it |
407
+ | `/users/u1/inbox/*` | direct children only (`*` = one path segment) |
408
+ | `/users/u1/inbox/**` | the whole subtree, any depth (`**` = recursive) |
409
+
410
+ Inbox items nest (e.g. `inbox/chats/<conversation>/<message>`), so you almost
411
+ always want `/**`.
412
+
320
413
  ### Workspace Management
321
414
 
322
415
  ```typescript
@@ -446,104 +539,117 @@ const client = new RaisinClient('raisin://localhost:8080/sys/default', {
446
539
 
447
540
  ## Framework Integration
448
541
 
542
+ The SDK ships dedicated subpath exports — `@raisindb/client/react`,
543
+ `@raisindb/client/svelte`, and `@raisindb/client/vue`. None of them make the
544
+ framework a dependency: React and Vue use a "bring your own framework"
545
+ factory (you pass the module in once), Svelte gets framework-free adapters
546
+ that bind naturally to runes.
547
+
449
548
  ### React
450
549
 
451
550
  ```tsx
452
- import { useEffect, useState } from 'react';
453
- import { RaisinClient, LocalStorageTokenStorage, IdentityUser } from '@raisindb/client';
551
+ // lib/raisin-react.ts
552
+ import React from 'react';
553
+ import { RaisinClient, LocalStorageTokenStorage } from '@raisindb/client';
554
+ import { createRaisinReact } from '@raisindb/client/react';
454
555
 
455
- const client = new RaisinClient('ws://localhost:8081/sys/default/myrepo', {
556
+ export const client = new RaisinClient('ws://localhost:8081/sys/default/myrepo', {
456
557
  tokenStorage: new LocalStorageTokenStorage('myapp'),
457
558
  });
458
559
 
459
- function App() {
460
- const [user, setUser] = useState<IdentityUser | null>(null);
461
- const [loading, setLoading] = useState(true);
462
-
463
- useEffect(() => {
464
- // Initialize session on mount
465
- client.initSession('myrepo').then(setUser).finally(() => setLoading(false));
560
+ export const {
561
+ RaisinProvider, useAuth, useConnection, useSql, useSubscription,
562
+ useConversation, useConversationList,
563
+ } = createRaisinReact(React);
466
564
 
467
- // Listen to auth state changes
468
- const unsubscribe = client.onAuthStateChange(({ event, session }) => {
469
- setUser(session.user);
470
- });
565
+ // App.tsx
566
+ function Chat() {
567
+ const chat = useConversation({
568
+ database: client.database('myrepo'),
569
+ createOptions: { participant: '/agents/support' },
570
+ });
471
571
 
472
- return unsubscribe;
473
- }, []);
572
+ return (
573
+ <div>
574
+ {chat.messages.map((m, i) => <p key={i}>{m.content}</p>)}
575
+ {chat.isStreaming && <p className="streaming">{chat.streamingText}</p>}
576
+ <button onClick={() => chat.sendMessage('Hello!')}>Send</button>
577
+ </div>
578
+ );
579
+ }
474
580
 
475
- if (loading) return <div>Loading...</div>;
476
- if (!user) return <LoginPage />;
477
- return <Dashboard user={user} />;
581
+ export default function App() {
582
+ return (
583
+ <RaisinProvider client={client} repository="myrepo">
584
+ <Chat />
585
+ </RaisinProvider>
586
+ );
478
587
  }
479
588
  ```
480
589
 
481
- ### SvelteKit
590
+ ### Svelte 5
482
591
 
483
592
  ```typescript
484
- // lib/raisin.ts
485
- import { RaisinClient, LocalStorageTokenStorage } from '@raisindb/client';
486
- import { writable } from 'svelte/store';
487
-
488
- export const client = new RaisinClient('ws://localhost:8081/sys/default/myrepo', {
489
- tokenStorage: new LocalStorageTokenStorage('myapp'),
490
- });
491
-
492
- export const user = writable<IdentityUser | null>(null);
493
-
494
- // Subscribe to auth changes
495
- client.onAuthStateChange(({ session }) => {
496
- user.set(session.user);
593
+ // lib/chat.svelte.ts
594
+ import { ConversationStore, type ConversationStoreSnapshot } from '@raisindb/client';
595
+ import { createConversationAdapter } from '@raisindb/client/svelte';
596
+ import { db } from '$lib/raisin';
597
+
598
+ const adapter = createConversationAdapter({
599
+ database: db,
600
+ createOptions: { participant: '/agents/support' },
497
601
  });
498
602
 
499
- // lib/stores/auth.ts
500
- export async function initSession() {
501
- return client.initSession('myrepo');
502
- }
503
-
504
- export async function login(email: string, password: string) {
505
- return client.loginWithEmail(email, password, 'myrepo');
506
- }
507
-
508
- export async function logout() {
509
- return client.logout();
510
- }
603
+ let snapshot = $state<ConversationStoreSnapshot>(adapter.getSnapshot());
604
+ adapter.subscribe((s) => { snapshot = s; });
605
+
606
+ export const chat = {
607
+ get messages() { return snapshot.messages; },
608
+ get isStreaming() { return snapshot.isStreaming; },
609
+ get streamingText() { return snapshot.streamingText; },
610
+ get plans() { return snapshot.plans; },
611
+ send: adapter.sendMessage,
612
+ approvePlan: adapter.approvePlan,
613
+ destroy: adapter.destroy,
614
+ };
511
615
  ```
512
616
 
513
617
  ### Vue 3
514
618
 
515
619
  ```typescript
516
- // composables/useAuth.ts
517
- import { ref, onMounted, onUnmounted } from 'vue';
518
- import { RaisinClient, LocalStorageTokenStorage, IdentityUser } from '@raisindb/client';
620
+ // lib/raisin-vue.ts
621
+ import * as vue from 'vue';
622
+ import { RaisinClient, LocalStorageTokenStorage } from '@raisindb/client';
623
+ import { createRaisinVue } from '@raisindb/client/vue';
519
624
 
520
- const client = new RaisinClient('ws://localhost:8081/sys/default/myrepo', {
625
+ export const client = new RaisinClient('ws://localhost:8081/sys/default/myrepo', {
521
626
  tokenStorage: new LocalStorageTokenStorage('myapp'),
522
627
  });
628
+ export const db = client.database('myrepo');
523
629
 
524
- export function useAuth() {
525
- const user = ref<IdentityUser | null>(null);
526
- const loading = ref(true);
527
- let unsubscribe: (() => void) | null = null;
528
-
529
- onMounted(async () => {
530
- user.value = await client.initSession('myrepo');
531
- loading.value = false;
532
-
533
- unsubscribe = client.onAuthStateChange(({ session }) => {
534
- user.value = session.user;
535
- });
536
- });
630
+ export const {
631
+ useAuth, useConnection, useSql, useSubscription,
632
+ useConversation, useConversationList,
633
+ } = createRaisinVue(vue);
634
+ ```
537
635
 
538
- onUnmounted(() => unsubscribe?.());
636
+ ```vue
637
+ <!-- Chat.vue -->
638
+ <script setup lang="ts">
639
+ import { db, useConversation } from '@/lib/raisin-vue';
539
640
 
540
- return {
541
- user,
542
- loading,
543
- login: (email: string, password: string) => client.loginWithEmail(email, password, 'myrepo'),
544
- logout: () => client.logout(),
545
- };
546
- }
641
+ const chat = useConversation({
642
+ database: db,
643
+ createOptions: { participant: '/agents/support' },
644
+ });
645
+ // Cleanup is automatic: the composable destroys its store onUnmounted.
646
+ </script>
647
+
648
+ <template>
649
+ <p v-for="(m, i) in chat.messages.value" :key="i">{{ m.content }}</p>
650
+ <p v-if="chat.isStreaming.value" class="streaming">{{ chat.streamingText.value }}</p>
651
+ <button @click="chat.sendMessage('Hello!')">Send</button>
652
+ </template>
547
653
  ```
548
654
 
549
655
  ### Server-Side Rendering (SSR)
@@ -723,6 +829,111 @@ npm run dev
723
829
  npm run typecheck
724
830
  ```
725
831
 
832
+ ## Stability options
833
+
834
+ Options and behaviors that keep chat, inbox, and workflow UIs responsive when
835
+ the network or server misbehaves.
836
+
837
+ ### `requestTimeout` (RaisinClient / RaisinHttpClient option)
838
+
839
+ Default: `30000` ms. Applies to:
840
+
841
+ - Every WebSocket request (resolved/rejected via the internal request tracker).
842
+ - All HTTP auth calls (`loginWithEmail`, `registerWithEmail`, `refreshToken`,
843
+ `initSession` token refresh) and `signAssetUrl` — these abort the underlying
844
+ `fetch` after `requestTimeout` and reject with a `RaisinTimeoutError`
845
+ (`code: 'REQUEST_TIMEOUT'`) instead of hanging forever.
846
+
847
+ Auth failures from the identity endpoints are thrown as `RaisinAuthError`
848
+ instances (real `Error` subclasses) that still carry the `{ code, message }`
849
+ fields, so existing destructuring keeps working:
850
+
851
+ ```typescript
852
+ try {
853
+ await client.loginWithEmail(email, password, 'my-repo');
854
+ } catch (err) {
855
+ if (err instanceof RaisinAuthError) {
856
+ console.log(err.code, err.message, err.status);
857
+ }
858
+ }
859
+ ```
860
+
861
+ ### `sendMessage` inactivity timeout (per-turn SSE)
862
+
863
+ `db.conversations.sendMessage(path, text, options)` streams the assistant turn
864
+ over a single-shot SSE connection. If that stream dies mid-turn (proxy reset,
865
+ server restart, dead TCP), the turn now ends with a synthetic `waiting` event
866
+ after `inactivityTimeoutMs` of silence (default: `120000` ms, reset on every
867
+ byte received) instead of stalling the chat UI forever:
868
+
869
+ ```typescript
870
+ for await (const event of db.conversations.sendMessage(path, 'Hi', {
871
+ inactivityTimeoutMs: 60_000, // override; 0 disables
872
+ })) {
873
+ // ... a final { type: 'waiting' } event is guaranteed even if the stream dies
874
+ }
875
+ ```
876
+
877
+ The same option exists on the lower-level `SSEClient` (`inactivityTimeoutMs`).
878
+ With reconnection enabled the SSE client silently reconnects on inactivity;
879
+ with reconnection disabled the stream ends with a `RaisinTimeoutError`
880
+ (`code: 'SSE_INACTIVITY_TIMEOUT'`).
881
+
882
+ ### `streamingTimeoutMs` + `watchdogIntervalMs` (ConversationStore)
883
+
884
+ `ConversationStore` adds two recovery layers on top of the SSE stream:
885
+
886
+ - `streamingTimeoutMs` (default: `120000`) — if no SSE event arrives for this
887
+ long while a turn is streaming, the store auto-recovers (reloads messages
888
+ and clears the streaming state).
889
+ - `watchdogIntervalMs` (default: `30000`) — while streaming, the store
890
+ periodically asks the backend whether the turn is actually still active
891
+ (`checkTurnHealth`) and recovers if the backend says it finished.
892
+
893
+ ```typescript
894
+ const store = new ConversationStore({
895
+ database: db,
896
+ conversationPath,
897
+ streamingTimeoutMs: 90_000,
898
+ watchdogIntervalMs: 15_000,
899
+ });
900
+ ```
901
+
902
+ ### Request queueing during reconnects
903
+
904
+ Requests issued while the WebSocket is temporarily down are no longer thrown
905
+ away. If the connection state is `Connecting`/`Reconnecting` (or
906
+ `Disconnected` with auto-reconnect enabled after a previous successful
907
+ connection), `RaisinClient` holds the request and flushes it once the
908
+ connection is re-established **and** re-authenticated:
909
+
910
+ - Each queued request still honors its own `requestTimeout` — it rejects if
911
+ the connection doesn't come back in time.
912
+ - The queue is bounded (100 requests); overflowing requests reject
913
+ immediately with a "Request queue is full" error.
914
+ - If re-authentication fails after reconnect, queued requests reject with a
915
+ `RaisinAuthError`.
916
+ - A permanent disconnect (manual `disconnect()`, max reconnect attempts
917
+ reached) cancels everything in the queue.
918
+
919
+ ### `subscription_restore_failed` event
920
+
921
+ After a reconnect, the client restores event subscriptions automatically and
922
+ retries on failure (3 retries with 1s/3s/9s backoff, without duplicating the
923
+ subscriptions that already succeeded). If restoration still fails, the client
924
+ emits `subscription_restore_failed` so the app can resync its state:
925
+
926
+ ```typescript
927
+ client.on('subscription_restore_failed', (error) => {
928
+ // Realtime events may be stale: reload lists and/or re-subscribe manually
929
+ console.warn('Subscriptions not restored after reconnect:', error);
930
+ });
931
+
932
+ client.onReconnected(() => {
933
+ // Fired only after connection + auth + successful subscription restore
934
+ });
935
+ ```
936
+
726
937
  ## License
727
938
 
728
939
  BSL-1.1 - See LICENSE file for details.