@raisindb/client 0.1.0 → 0.1.1

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 +313 -70
  2. package/dist/chunk-ASOWGNQZ.mjs +532 -0
  3. package/dist/chunk-ASOWGNQZ.mjs.map +1 -0
  4. package/dist/chunk-FYWYSNQG.mjs +190 -0
  5. package/dist/chunk-FYWYSNQG.mjs.map +1 -0
  6. package/dist/chunk-NOPZAMIV.mjs +1427 -0
  7. package/dist/chunk-NOPZAMIV.mjs.map +1 -0
  8. package/dist/chunk-T2ZSNYFG.mjs +248 -0
  9. package/dist/chunk-T2ZSNYFG.mjs.map +1 -0
  10. package/dist/chunk-TPZNJNZR.mjs +866 -0
  11. package/dist/chunk-TPZNJNZR.mjs.map +1 -0
  12. package/dist/chunk-ZCRYQCXS.mjs +170 -0
  13. package/dist/chunk-ZCRYQCXS.mjs.map +1 -0
  14. package/dist/conversation-list-store-D-W6_nsv.d.mts +8447 -0
  15. package/dist/conversation-list-store-D-W6_nsv.d.ts +8447 -0
  16. package/dist/index-B4CqE3Cb.d.ts +301 -0
  17. package/dist/index-CeBRaSop.d.mts +301 -0
  18. package/dist/index.d.mts +72 -6109
  19. package/dist/index.d.ts +72 -6109
  20. package/dist/index.js +3123 -257
  21. package/dist/index.js.map +1 -0
  22. package/dist/index.mjs +2745 -3185
  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 +2569 -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-CQoB4FEm.d.mts +52 -0
  31. package/dist/svelte-sql-CTXjkz0R.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 +2482 -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 +1510 -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,131 @@ 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
+ ### Chat without login (a public website)
373
+
374
+ An anonymous connection can chat with an agent that allows anonymous visitors
375
+ (`anonymous.enabled: true` on its `raisin:AIAgent`). No login, no server
376
+ endpoint of your own: the conversation lives in an ephemeral visitor session
377
+ that only this browser tab can read.
378
+
379
+ ```typescript
380
+ const client = new RaisinClient('wss://db.example.com/ws/website');
381
+ await client.connect(); // anonymous
382
+ const db = client.database('website');
383
+
384
+ const convo = await db.conversations.startAnonymous('/agents/website-assistant');
385
+ for await (const ev of db.conversations.sendMessage(convo.conversationPath, 'Hello')) {
386
+ if (ev.type === 'text_chunk') append(ev.text);
387
+ if (ev.type === 'failed') showLimit(ev.code); // RATE_LIMITED, BUSY, LIMIT_REACHED, …
388
+ }
389
+
390
+ // Or the store / hooks, with one extra option:
391
+ new ConversationStore({
392
+ database: db,
393
+ createOptions: { participant: '/agents/website-assistant', anonymous: true },
394
+ });
395
+ ```
396
+
397
+ The session secret and the conversation id are kept in `sessionStorage`
398
+ (`options.store` to change that), so a reload continues the chat, and the SDK
399
+ re-proves the session after a reconnect. Server-side limits (message size and
400
+ count, token budget, rate, one turn at a time) surface as `failed` events with
401
+ a `code`, or as a thrown `VisitorChatError`. See
402
+ `docs/developer/messaging/anonymous-visitors.md` in the RaisinDB repository.
403
+
404
+ ### Listen to the inbox (notifications without an extra API)
405
+
406
+ Server-side messaging delivers items into the logged-in user's home inbox in
407
+ the `raisin:access_control` workspace. A plain node subscription on
408
+ `${home}/inbox/**` is all an inbox bell needs — no polling, no extra API.
409
+
410
+ ```typescript
411
+ import { normalizeHomePath } from '@raisindb/client';
412
+
413
+ const user = await client.initSession('myrepo');
414
+
415
+ // user.home may be workspace-prefixed (/raisin:access_control/users/...).
416
+ // Subscription paths must be workspace-relative — always normalize.
417
+ const home = normalizeHomePath(user!.home)!;
418
+
419
+ await db.workspace('raisin:access_control').events().subscribe(
420
+ {
421
+ path: `${home}/inbox/**`,
422
+ event_types: ['node:created'],
423
+ include_node: true, // deliver the full node so we can render a title
424
+ },
425
+ (event) => {
426
+ const props = event.payload.node?.properties ?? {};
427
+ if (props.role === 'user') return; // skip the user's own chat messages
428
+ bumpBadge();
429
+ showToast((props.title as string) ?? (props.subject as string) ?? 'New inbox item');
430
+ },
431
+ );
432
+ ```
433
+
434
+ **Path filter semantics** (important — there is no implicit prefix matching):
435
+
436
+ | Pattern | Matches |
437
+ |---------|---------|
438
+ | `/users/u1/inbox` | exactly that node, nothing below it |
439
+ | `/users/u1/inbox/*` | direct children only (`*` = one path segment) |
440
+ | `/users/u1/inbox/**` | the whole subtree, any depth (`**` = recursive) |
441
+
442
+ Inbox items nest (e.g. `inbox/chats/<conversation>/<message>`), so you almost
443
+ always want `/**`.
444
+
320
445
  ### Workspace Management
321
446
 
322
447
  ```typescript
@@ -446,104 +571,117 @@ const client = new RaisinClient('raisin://localhost:8080/sys/default', {
446
571
 
447
572
  ## Framework Integration
448
573
 
574
+ The SDK ships dedicated subpath exports — `@raisindb/client/react`,
575
+ `@raisindb/client/svelte`, and `@raisindb/client/vue`. None of them make the
576
+ framework a dependency: React and Vue use a "bring your own framework"
577
+ factory (you pass the module in once), Svelte gets framework-free adapters
578
+ that bind naturally to runes.
579
+
449
580
  ### React
450
581
 
451
582
  ```tsx
452
- import { useEffect, useState } from 'react';
453
- import { RaisinClient, LocalStorageTokenStorage, IdentityUser } from '@raisindb/client';
583
+ // lib/raisin-react.ts
584
+ import React from 'react';
585
+ import { RaisinClient, LocalStorageTokenStorage } from '@raisindb/client';
586
+ import { createRaisinReact } from '@raisindb/client/react';
454
587
 
455
- const client = new RaisinClient('ws://localhost:8081/sys/default/myrepo', {
588
+ export const client = new RaisinClient('ws://localhost:8081/sys/default/myrepo', {
456
589
  tokenStorage: new LocalStorageTokenStorage('myapp'),
457
590
  });
458
591
 
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));
592
+ export const {
593
+ RaisinProvider, useAuth, useConnection, useSql, useSubscription,
594
+ useConversation, useConversationList,
595
+ } = createRaisinReact(React);
466
596
 
467
- // Listen to auth state changes
468
- const unsubscribe = client.onAuthStateChange(({ event, session }) => {
469
- setUser(session.user);
470
- });
597
+ // App.tsx
598
+ function Chat() {
599
+ const chat = useConversation({
600
+ database: client.database('myrepo'),
601
+ createOptions: { participant: '/agents/support' },
602
+ });
471
603
 
472
- return unsubscribe;
473
- }, []);
604
+ return (
605
+ <div>
606
+ {chat.messages.map((m, i) => <p key={i}>{m.content}</p>)}
607
+ {chat.isStreaming && <p className="streaming">{chat.streamingText}</p>}
608
+ <button onClick={() => chat.sendMessage('Hello!')}>Send</button>
609
+ </div>
610
+ );
611
+ }
474
612
 
475
- if (loading) return <div>Loading...</div>;
476
- if (!user) return <LoginPage />;
477
- return <Dashboard user={user} />;
613
+ export default function App() {
614
+ return (
615
+ <RaisinProvider client={client} repository="myrepo">
616
+ <Chat />
617
+ </RaisinProvider>
618
+ );
478
619
  }
479
620
  ```
480
621
 
481
- ### SvelteKit
622
+ ### Svelte 5
482
623
 
483
624
  ```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'),
625
+ // lib/chat.svelte.ts
626
+ import { ConversationStore, type ConversationStoreSnapshot } from '@raisindb/client';
627
+ import { createConversationAdapter } from '@raisindb/client/svelte';
628
+ import { db } from '$lib/raisin';
629
+
630
+ const adapter = createConversationAdapter({
631
+ database: db,
632
+ createOptions: { participant: '/agents/support' },
490
633
  });
491
634
 
492
- export const user = writable<IdentityUser | null>(null);
493
-
494
- // Subscribe to auth changes
495
- client.onAuthStateChange(({ session }) => {
496
- user.set(session.user);
497
- });
498
-
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
- }
635
+ let snapshot = $state<ConversationStoreSnapshot>(adapter.getSnapshot());
636
+ adapter.subscribe((s) => { snapshot = s; });
637
+
638
+ export const chat = {
639
+ get messages() { return snapshot.messages; },
640
+ get isStreaming() { return snapshot.isStreaming; },
641
+ get streamingText() { return snapshot.streamingText; },
642
+ get plans() { return snapshot.plans; },
643
+ send: adapter.sendMessage,
644
+ approvePlan: adapter.approvePlan,
645
+ destroy: adapter.destroy,
646
+ };
511
647
  ```
512
648
 
513
649
  ### Vue 3
514
650
 
515
651
  ```typescript
516
- // composables/useAuth.ts
517
- import { ref, onMounted, onUnmounted } from 'vue';
518
- import { RaisinClient, LocalStorageTokenStorage, IdentityUser } from '@raisindb/client';
652
+ // lib/raisin-vue.ts
653
+ import * as vue from 'vue';
654
+ import { RaisinClient, LocalStorageTokenStorage } from '@raisindb/client';
655
+ import { createRaisinVue } from '@raisindb/client/vue';
519
656
 
520
- const client = new RaisinClient('ws://localhost:8081/sys/default/myrepo', {
657
+ export const client = new RaisinClient('ws://localhost:8081/sys/default/myrepo', {
521
658
  tokenStorage: new LocalStorageTokenStorage('myapp'),
522
659
  });
660
+ export const db = client.database('myrepo');
523
661
 
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
- });
662
+ export const {
663
+ useAuth, useConnection, useSql, useSubscription,
664
+ useConversation, useConversationList,
665
+ } = createRaisinVue(vue);
666
+ ```
537
667
 
538
- onUnmounted(() => unsubscribe?.());
668
+ ```vue
669
+ <!-- Chat.vue -->
670
+ <script setup lang="ts">
671
+ import { db, useConversation } from '@/lib/raisin-vue';
539
672
 
540
- return {
541
- user,
542
- loading,
543
- login: (email: string, password: string) => client.loginWithEmail(email, password, 'myrepo'),
544
- logout: () => client.logout(),
545
- };
546
- }
673
+ const chat = useConversation({
674
+ database: db,
675
+ createOptions: { participant: '/agents/support' },
676
+ });
677
+ // Cleanup is automatic: the composable destroys its store onUnmounted.
678
+ </script>
679
+
680
+ <template>
681
+ <p v-for="(m, i) in chat.messages.value" :key="i">{{ m.content }}</p>
682
+ <p v-if="chat.isStreaming.value" class="streaming">{{ chat.streamingText.value }}</p>
683
+ <button @click="chat.sendMessage('Hello!')">Send</button>
684
+ </template>
547
685
  ```
548
686
 
549
687
  ### Server-Side Rendering (SSR)
@@ -723,6 +861,111 @@ npm run dev
723
861
  npm run typecheck
724
862
  ```
725
863
 
864
+ ## Stability options
865
+
866
+ Options and behaviors that keep chat, inbox, and workflow UIs responsive when
867
+ the network or server misbehaves.
868
+
869
+ ### `requestTimeout` (RaisinClient / RaisinHttpClient option)
870
+
871
+ Default: `30000` ms. Applies to:
872
+
873
+ - Every WebSocket request (resolved/rejected via the internal request tracker).
874
+ - All HTTP auth calls (`loginWithEmail`, `registerWithEmail`, `refreshToken`,
875
+ `initSession` token refresh) and `signAssetUrl` — these abort the underlying
876
+ `fetch` after `requestTimeout` and reject with a `RaisinTimeoutError`
877
+ (`code: 'REQUEST_TIMEOUT'`) instead of hanging forever.
878
+
879
+ Auth failures from the identity endpoints are thrown as `RaisinAuthError`
880
+ instances (real `Error` subclasses) that still carry the `{ code, message }`
881
+ fields, so existing destructuring keeps working:
882
+
883
+ ```typescript
884
+ try {
885
+ await client.loginWithEmail(email, password, 'my-repo');
886
+ } catch (err) {
887
+ if (err instanceof RaisinAuthError) {
888
+ console.log(err.code, err.message, err.status);
889
+ }
890
+ }
891
+ ```
892
+
893
+ ### `sendMessage` inactivity timeout (per-turn SSE)
894
+
895
+ `db.conversations.sendMessage(path, text, options)` streams the assistant turn
896
+ over a single-shot SSE connection. If that stream dies mid-turn (proxy reset,
897
+ server restart, dead TCP), the turn now ends with a synthetic `waiting` event
898
+ after `inactivityTimeoutMs` of silence (default: `120000` ms, reset on every
899
+ byte received) instead of stalling the chat UI forever:
900
+
901
+ ```typescript
902
+ for await (const event of db.conversations.sendMessage(path, 'Hi', {
903
+ inactivityTimeoutMs: 60_000, // override; 0 disables
904
+ })) {
905
+ // ... a final { type: 'waiting' } event is guaranteed even if the stream dies
906
+ }
907
+ ```
908
+
909
+ The same option exists on the lower-level `SSEClient` (`inactivityTimeoutMs`).
910
+ With reconnection enabled the SSE client silently reconnects on inactivity;
911
+ with reconnection disabled the stream ends with a `RaisinTimeoutError`
912
+ (`code: 'SSE_INACTIVITY_TIMEOUT'`).
913
+
914
+ ### `streamingTimeoutMs` + `watchdogIntervalMs` (ConversationStore)
915
+
916
+ `ConversationStore` adds two recovery layers on top of the SSE stream:
917
+
918
+ - `streamingTimeoutMs` (default: `120000`) — if no SSE event arrives for this
919
+ long while a turn is streaming, the store auto-recovers (reloads messages
920
+ and clears the streaming state).
921
+ - `watchdogIntervalMs` (default: `30000`) — while streaming, the store
922
+ periodically asks the backend whether the turn is actually still active
923
+ (`checkTurnHealth`) and recovers if the backend says it finished.
924
+
925
+ ```typescript
926
+ const store = new ConversationStore({
927
+ database: db,
928
+ conversationPath,
929
+ streamingTimeoutMs: 90_000,
930
+ watchdogIntervalMs: 15_000,
931
+ });
932
+ ```
933
+
934
+ ### Request queueing during reconnects
935
+
936
+ Requests issued while the WebSocket is temporarily down are no longer thrown
937
+ away. If the connection state is `Connecting`/`Reconnecting` (or
938
+ `Disconnected` with auto-reconnect enabled after a previous successful
939
+ connection), `RaisinClient` holds the request and flushes it once the
940
+ connection is re-established **and** re-authenticated:
941
+
942
+ - Each queued request still honors its own `requestTimeout` — it rejects if
943
+ the connection doesn't come back in time.
944
+ - The queue is bounded (100 requests); overflowing requests reject
945
+ immediately with a "Request queue is full" error.
946
+ - If re-authentication fails after reconnect, queued requests reject with a
947
+ `RaisinAuthError`.
948
+ - A permanent disconnect (manual `disconnect()`, max reconnect attempts
949
+ reached) cancels everything in the queue.
950
+
951
+ ### `subscription_restore_failed` event
952
+
953
+ After a reconnect, the client restores event subscriptions automatically and
954
+ retries on failure (3 retries with 1s/3s/9s backoff, without duplicating the
955
+ subscriptions that already succeeded). If restoration still fails, the client
956
+ emits `subscription_restore_failed` so the app can resync its state:
957
+
958
+ ```typescript
959
+ client.on('subscription_restore_failed', (error) => {
960
+ // Realtime events may be stale: reload lists and/or re-subscribe manually
961
+ console.warn('Subscriptions not restored after reconnect:', error);
962
+ });
963
+
964
+ client.onReconnected(() => {
965
+ // Fired only after connection + auth + successful subscription restore
966
+ });
967
+ ```
968
+
726
969
  ## License
727
970
 
728
971
  BSL-1.1 - See LICENSE file for details.