@marktiderman/genesis-chat 0.2.0 → 0.2.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 (77) hide show
  1. package/README.md +82 -7
  2. package/dist/{chunk-5D5A7I4Q.js → chunk-A4YJX7KT.js} +130 -16
  3. package/dist/chunk-A4YJX7KT.js.map +1 -0
  4. package/dist/chunk-AZS3HNLM.js +30 -0
  5. package/dist/chunk-AZS3HNLM.js.map +1 -0
  6. package/dist/{chunk-7S6HYNWW.js → chunk-GBJ3YI4E.js} +72 -5
  7. package/dist/chunk-GBJ3YI4E.js.map +1 -0
  8. package/dist/{chunk-PZTOY2LY.js → chunk-JU2FAPBI.js} +46 -13
  9. package/dist/chunk-JU2FAPBI.js.map +1 -0
  10. package/dist/{client-DMa2j7kz.d.ts → client-DUIG1hum.d.ts} +36 -1
  11. package/dist/index.d.ts +4 -4
  12. package/dist/index.js +2 -2
  13. package/dist/native.d.ts +36 -7
  14. package/dist/native.js +239 -116
  15. package/dist/native.js.map +1 -1
  16. package/dist/{outbox-B0y3phE0.d.ts → outbox-B9NAWQ58.d.ts} +74 -6
  17. package/dist/push.d.ts +2 -2
  18. package/dist/react.d.ts +48 -21
  19. package/dist/react.js +3 -3
  20. package/dist/server.d.ts +43 -2
  21. package/dist/server.js +115 -15
  22. package/dist/server.js.map +1 -1
  23. package/dist/{thread-DWXHKPdw.d.ts → thread-oPra7pkW.d.ts} +1 -1
  24. package/dist/web.d.ts +31 -11
  25. package/dist/web.js +97 -48
  26. package/dist/web.js.map +1 -1
  27. package/migrations/0003_genesis_chat_search_threads.sql +85 -0
  28. package/migrations/tests/0002_genesis_chat_coaching.test.sql +7 -3
  29. package/migrations/tests/0003_genesis_chat_search_threads.test.sql +50 -0
  30. package/package.json +7 -3
  31. package/src/__tests__/coaching-core.test.ts +3 -1
  32. package/src/__tests__/coaching-react.test.tsx +73 -2
  33. package/src/__tests__/coaching-web.test.tsx +78 -6
  34. package/src/__tests__/fake-xhr.ts +54 -0
  35. package/src/__tests__/outbox-hosted.test.ts +269 -0
  36. package/src/__tests__/review-279.test.ts +41 -0
  37. package/src/__tests__/review-fixes-web.test.tsx +13 -10
  38. package/src/__tests__/server-fake-video.test.ts +81 -0
  39. package/src/__tests__/server-fakes.ts +7 -1
  40. package/src/__tests__/server-safety.test.ts +17 -4
  41. package/src/__tests__/web.test.tsx +16 -0
  42. package/src/core/client.ts +28 -2
  43. package/src/core/outbox.ts +222 -16
  44. package/src/core/transfer.ts +71 -0
  45. package/src/core/types.ts +17 -0
  46. package/src/index.ts +1 -0
  47. package/src/native/__tests__/emoji.test.tsx +16 -0
  48. package/src/native/__tests__/organise-screens.test.tsx +80 -0
  49. package/src/native/__tests__/video-media.test.tsx +81 -4
  50. package/src/native/attachment-cards.tsx +22 -6
  51. package/src/native/inbox-screen.tsx +12 -6
  52. package/src/native/index.ts +2 -2
  53. package/src/native/message-actions-sheet.tsx +10 -5
  54. package/src/native/message-bubble.tsx +3 -11
  55. package/src/native/modules.ts +2 -0
  56. package/src/native/schedule-sheet.tsx +23 -9
  57. package/src/native/search-screen.tsx +26 -5
  58. package/src/native/thread-screen.tsx +30 -3
  59. package/src/native/topics.tsx +70 -15
  60. package/src/native/video-player.tsx +46 -6
  61. package/src/native/video-recorder.tsx +6 -3
  62. package/src/native/voice-player.tsx +18 -0
  63. package/src/react/index.tsx +103 -19
  64. package/src/react/previews.ts +51 -0
  65. package/src/server/fake-video.ts +171 -0
  66. package/src/server/index.ts +1 -0
  67. package/src/web/attachments.tsx +6 -6
  68. package/src/web/bubble.tsx +22 -0
  69. package/src/web/inbox.tsx +7 -6
  70. package/src/web/memory-client.ts +11 -3
  71. package/src/web/panel.tsx +2 -2
  72. package/src/web/schedule.tsx +14 -2
  73. package/src/web/thread.tsx +36 -8
  74. package/src/web/video.tsx +28 -34
  75. package/dist/chunk-5D5A7I4Q.js.map +0 -1
  76. package/dist/chunk-7S6HYNWW.js.map +0 -1
  77. package/dist/chunk-PZTOY2LY.js.map +0 -1
@@ -1,4 +1,4 @@
1
- import { b as AttachmentKind, A as AttachmentInput, i as ChatClient, B as PendingMessage, P as PendingFile, q as ChatMessage, E as SendInput } from './client-DMa2j7kz.js';
1
+ import { b as AttachmentKind, A as AttachmentInput, U as UploadProgress, H as HostedAttachment, i as ChatClient, B as PendingMessage, P as PendingFile, q as ChatMessage, F as SendInput } from './client-DUIG1hum.js';
2
2
 
3
3
  /**
4
4
  * @feat:chat
@@ -62,6 +62,13 @@ declare function nextPlaybackRate(rate: number): number;
62
62
  * and only sent again by hand. `clear()` (sign-out) stops sends in flight: none
63
63
  * posts, or saves anything, afterwards. Storage is the platform's (memory by default;
64
64
  * localStorage on web, MMKV or SQLite on native), behind OutboxStore.
65
+ *
66
+ * A hosted video (`LocalFile.hosted`, with `hostedUpload`) doesn't wait for its bytes: the
67
+ * upload is started (the provider's upload id minted), the message is sent in its place in the
68
+ * order with the attachment `pending`, and the bytes carry on going up; the provider's webhook
69
+ * marks it ready. The message stays here (hidden from the thread once the server has it) until
70
+ * its bytes are up, so a relaunch carries the upload on (`resume`) and sends the message again
71
+ * with the same clientId, which posts nothing twice.
65
72
  */
66
73
 
67
74
  /** A file still on the device, uploaded when its message is sent. */
@@ -76,7 +83,47 @@ interface LocalFile {
76
83
  meta?: Partial<Omit<AttachmentInput, "path" | "kind">> & {
77
84
  kind?: AttachmentInput["kind"];
78
85
  };
86
+ /**
87
+ * Send it to the hosted video provider (the outbox's `hostedUpload`) instead of storage: the
88
+ * message goes at once, in order, with the attachment pending, while the bytes go up. Without
89
+ * a `hostedUpload` it goes to storage like any file.
90
+ */
91
+ hosted?: boolean;
92
+ }
93
+ /** A hosted upload as the outbox keeps it (saved with the message), so it can be carried on. */
94
+ interface HostedUploadRecord {
95
+ provider: HostedAttachment["provider"];
96
+ providerId: string;
97
+ /** Where the bytes go, when the hook returned it: what a resume needs. */
98
+ url?: string;
99
+ /** Every byte is there. */
100
+ uploaded?: boolean;
101
+ }
102
+ /** What `hostedUpload` resolves with as soon as the upload exists (before the bytes are up). */
103
+ interface HostedUploadStart extends HostedAttachment {
104
+ /** Where the bytes go; kept with the message, handed back as `resume.url`. */
105
+ url?: string;
106
+ /** Settles when every byte is there (rejects when the transfer fails). Absent: already there. */
107
+ done?: Promise<void>;
79
108
  }
109
+ interface HostedUploadOptions {
110
+ /** Aborts when the message is discarded or the outbox cleared. */
111
+ signal?: AbortSignal;
112
+ onProgress?: UploadProgress;
113
+ /**
114
+ * The upload started before (a relaunch, a retry after the transfer failed): carry it on and
115
+ * resolve with the same `providerId`. A message may already be posted with it, so mint a new
116
+ * one only when the old upload is gone; that message's video then stays pending.
117
+ */
118
+ resume?: HostedUploadRecord;
119
+ }
120
+ /** Starts (or carries on) a hosted upload; resolves once the provider's upload id exists. */
121
+ type HostedUpload = (conversationId: string, file: LocalFile, opts: HostedUploadOptions) => Promise<HostedUploadStart>;
122
+ /** Uploads a file to storage and resolves with its path. */
123
+ type StorageUpload = (conversationId: string, file: LocalFile, opts?: {
124
+ signal?: AbortSignal;
125
+ onProgress?: UploadProgress;
126
+ }) => Promise<string>;
80
127
  interface QueuedMessage extends Omit<PendingMessage, "files"> {
81
128
  files: LocalFile[];
82
129
  /** What each file is, kept with the saved copy (PendingMessage.files). */
@@ -84,6 +131,10 @@ interface QueuedMessage extends Omit<PendingMessage, "files"> {
84
131
  /** Files lost on a relaunch (bytes that weren't persisted). */
85
132
  lostFiles?: number;
86
133
  attempts: number;
134
+ /** Hosted uploads started, by file slot. */
135
+ hosted?: Record<number, HostedUploadRecord>;
136
+ /** Posted (this message's id): it waits here only for its hosted bytes to finish. */
137
+ sentMessageId?: string;
87
138
  }
88
139
  interface OutboxStore {
89
140
  load(): Promise<QueuedMessage[]>;
@@ -96,10 +147,18 @@ interface OutboxOptions {
96
147
  store?: OutboxStore;
97
148
  /** Reads a native file URI into bytes for upload. */
98
149
  readFile?: (uri: string) => Promise<Blob | ArrayBuffer>;
99
- /** Upload hook, e.g. resumable uploads on native. Defaults to client.upload. `signal` aborts when the message is discarded or the outbox cleared. */
100
- upload?: (conversationId: string, file: LocalFile, opts?: {
101
- signal?: AbortSignal;
102
- }) => Promise<string>;
150
+ /**
151
+ * Upload hook, e.g. resumable uploads on native. Defaults to client.upload. `signal` aborts
152
+ * when the message is discarded or the outbox cleared; call `onProgress(sent, total)` as bytes
153
+ * go, and the pending bubble shows it (`PendingMessage.files[i].progress`).
154
+ */
155
+ upload?: StorageUpload;
156
+ /**
157
+ * Starts a hosted video upload for a file marked `hosted` and resolves once the provider's
158
+ * upload id exists, with `done` for the bytes. See HostedUpload; without it, hosted files go
159
+ * to storage.
160
+ */
161
+ hostedUpload?: HostedUpload;
103
162
  /** Called with every change, for the UI. */
104
163
  onChange?: (queue: QueuedMessage[]) => void;
105
164
  /** Called when a message lands. */
@@ -141,6 +200,15 @@ declare function createOutbox(opts: OutboxOptions): {
141
200
  /** Forgets every queued message, here and in the store (call it on sign-out). */
142
201
  clear(): Promise<void>;
143
202
  discard(clientId: string): Promise<void>;
203
+ /**
204
+ * Uploads one file to storage the way the outbox does (the `upload` hook, or `readFile` then
205
+ * the client's upload) and resolves with its path: for files sent outside the queue, such as
206
+ * a scheduled message's.
207
+ */
208
+ uploadFile(conversationId: string, file: LocalFile, o?: {
209
+ signal?: AbortSignal;
210
+ onProgress?: UploadProgress;
211
+ }): Promise<string>;
144
212
  pending(conversationId?: string): PendingMessage[];
145
213
  /** A new client id, made the way this outbox makes them. */
146
214
  newId: typeof randomId;
@@ -148,4 +216,4 @@ declare function createOutbox(opts: OutboxOptions): {
148
216
  };
149
217
  type Outbox = ReturnType<typeof createOutbox>;
150
218
 
151
- export { ALLOWED_EXTENSIONS as A, CHAT_LIMITS as C, type LocalFile as L, type Outbox as O, PLAYBACK_RATES as P, type QueuedMessage as Q, type OutboxOptions as a, type OutboxStore as b, attachmentKindFor as c, createOutbox as d, formatDuration as e, fileExtension as f, recordingLimitMs as g, releaseObjectUrl as h, isAllowedExtension as i, waveformPeaks as j, memoryOutboxStore as m, nextPlaybackRate as n, randomId as r, waveformFromMetering as w };
219
+ export { ALLOWED_EXTENSIONS as A, CHAT_LIMITS as C, type HostedUpload as H, type LocalFile as L, type Outbox as O, PLAYBACK_RATES as P, type QueuedMessage as Q, type StorageUpload as S, type HostedUploadOptions as a, type HostedUploadRecord as b, type HostedUploadStart as c, type OutboxOptions as d, type OutboxStore as e, attachmentKindFor as f, createOutbox as g, fileExtension as h, formatDuration as i, isAllowedExtension as j, recordingLimitMs as k, releaseObjectUrl as l, memoryOutboxStore as m, nextPlaybackRate as n, waveformPeaks as o, randomId as r, waveformFromMetering as w };
package/dist/push.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { i as ChatClient } from './client-DMa2j7kz.js';
2
- import { O as Outbox, b as OutboxStore, Q as QueuedMessage } from './outbox-B0y3phE0.js';
1
+ import { i as ChatClient } from './client-DUIG1hum.js';
2
+ import { O as Outbox, e as OutboxStore, Q as QueuedMessage } from './outbox-B9NAWQ58.js';
3
3
  import '@supabase/supabase-js';
4
4
 
5
5
  /**
package/dist/react.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  import * as _tanstack_query_core from '@tanstack/query-core';
2
- import { h as ChatAttachment, i as ChatClient, k as ChatConversation, p as ChatMember, t as ChatRole, n as ChatKindRules, q as ChatMessage, g as ChatActivity, E as SendInput, M as MessagePage, C as ChatAvailability, d as AvailabilityInput, v as ChatSignal, m as ChatInboxItem, o as ChatLinkPreview, D as ScheduledMessage, S as ScheduleInput, u as ChatSearchHit, x as ChatTopic, T as TopicInput } from './client-DMa2j7kz.js';
2
+ import { h as ChatAttachment, i as ChatClient, k as ChatConversation, p as ChatMember, t as ChatRole, n as ChatKindRules, q as ChatMessage, g as ChatActivity, F as SendInput, M as MessagePage, C as ChatAvailability, d as AvailabilityInput, v as ChatSignal, m as ChatInboxItem, o as ChatLinkPreview, D as ScheduledMessage, S as ScheduleInput, u as ChatSearchHit, x as ChatTopic, T as TopicInput } from './client-DUIG1hum.js';
3
3
  import * as react from 'react';
4
4
  import { ReactNode } from 'react';
5
- import { b as OutboxStore, L as LocalFile, O as Outbox, Q as QueuedMessage } from './outbox-B0y3phE0.js';
6
- import { T as ThreadItem } from './thread-DWXHKPdw.js';
5
+ import { e as OutboxStore, S as StorageUpload, H as HostedUpload, L as LocalFile, O as Outbox, Q as QueuedMessage } from './outbox-B9NAWQ58.js';
6
+ import { T as ThreadItem } from './thread-oPra7pkW.js';
7
7
  import '@supabase/supabase-js';
8
8
 
9
9
  declare const chatKeys: {
@@ -33,9 +33,11 @@ type MessagesData = MessagePage;
33
33
  /** Does a messages list (`[genesis-chat, conversation, id, messages, threadRootId, topic]`) hold this message? */
34
34
  declare function belongsTo(key: readonly unknown[], m: ChatMessage): boolean;
35
35
  /**
36
- * Hosted video uploads (Mux direct uploads): your server makes an upload and returns its URL;
37
- * the renderer PUTs the recording there and sends `{ kind: "video", provider: "mux", providerId,
38
- * status: "pending" }`. Without it, videos go to storage as files.
36
+ * Hosted video uploads (Mux direct uploads): your server makes an upload and returns its URL
37
+ * (`createVideoUploadHandler` in /server). A video the composer marks `hosted` is sent through
38
+ * the outbox at once, in order, with `{ kind: "video", provider, providerId, status: "pending" }`,
39
+ * while its bytes are PUT to that URL (see `hostedUploadFrom`). Without it, videos go to storage
40
+ * as files.
39
41
  */
40
42
  interface ChatVideoUpload {
41
43
  create(conversationId: string): Promise<{
@@ -44,16 +46,33 @@ interface ChatVideoUpload {
44
46
  provider: "mux";
45
47
  }>;
46
48
  }
49
+ /**
50
+ * The outbox's `hostedUpload` from a `ChatVideoUpload`: mints the upload (or, resuming, reuses
51
+ * the one minted before), resolves at once with its id, and PUTs the whole file to its URL in
52
+ * the background (XMLHttpRequest where there is one, for progress). A native file URI is read
53
+ * with `readFile`. Native apps with chunked, resumable uploads pass their own `hostedUpload`.
54
+ */
55
+ declare function hostedUploadFrom(videoUpload: ChatVideoUpload, options?: {
56
+ readFile?: (uri: string) => Promise<Blob | ArrayBuffer>;
57
+ fetch?: typeof fetch;
58
+ }): HostedUpload;
59
+ /** What a kind's `title`, `subtitle` and `action` are told besides the attachment. */
60
+ interface ChatCardContext {
61
+ /** The card is on a message you sent (your own check-in reads "Sent", not "Answer"). */
62
+ mine: boolean;
63
+ }
47
64
  /** What an app attachment kind's card shows (a form, a check-in): plain data, drawn by each renderer. */
48
65
  interface ChatAttachmentKind {
49
- title(attachment: ChatAttachment): string;
50
- subtitle?(attachment: ChatAttachment): string | null | undefined;
66
+ title(attachment: ChatAttachment, ctx?: ChatCardContext): string;
67
+ subtitle?(attachment: ChatAttachment, ctx?: ChatCardContext): string | null | undefined;
51
68
  /** The card's button: a link (`href`) or a handler (`onPress`). */
52
- action?(attachment: ChatAttachment): {
69
+ action?(attachment: ChatAttachment, ctx?: ChatCardContext): {
53
70
  label: string;
54
71
  href?: string;
55
72
  onPress?: () => void;
56
73
  } | null | undefined;
74
+ /** What previews call it (the inbox's last message, "Check-in"); the title by default. */
75
+ label?: string;
57
76
  }
58
77
  interface ChatContextValue {
59
78
  client: ChatClient;
@@ -78,10 +97,17 @@ interface ChatProviderProps {
78
97
  outboxStore?: OutboxStore;
79
98
  /** Reads a native file URI for upload. */
80
99
  readFile?: (uri: string) => Promise<Blob | ArrayBuffer>;
81
- /** Replaces the plain upload (resumable uploads on native). `signal` aborts when the message is discarded or the outbox cleared. */
82
- upload?: (conversationId: string, file: LocalFile, opts?: {
83
- signal?: AbortSignal;
84
- }) => Promise<string>;
100
+ /**
101
+ * Replaces the plain upload (resumable uploads on native). `signal` aborts when the message is
102
+ * discarded or the outbox cleared; `onProgress(sent, total)` fills the pending bubble's bar.
103
+ */
104
+ upload?: StorageUpload;
105
+ /**
106
+ * Starts a hosted upload for files marked `hosted` (see HostedUpload): resolve with
107
+ * `{ provider, providerId, status: "pending", url?, done }` once the upload id exists. Default:
108
+ * `hostedUploadFrom(videoUpload)` when `videoUpload` is given.
109
+ */
110
+ hostedUpload?: HostedUpload;
85
111
  /** Makes client ids (default: a random UUID that works without crypto.randomUUID). */
86
112
  newId?: () => string;
87
113
  /** Keeps a byte file (a pasted image, a recording) across launches: write it, return a URI. */
@@ -102,7 +128,7 @@ interface ChatPerson {
102
128
  avatarUrl?: string | null;
103
129
  }
104
130
  /** Puts chat in reach of every hook below it. Needs a TanStack QueryClientProvider above it. */
105
- declare function ChatProvider({ client, userId, outboxStore, readFile, upload, newId, persistFile, releasePreview, loadPeople, videoUpload, attachmentKinds, children, }: ChatProviderProps): react.JSX.Element;
131
+ declare function ChatProvider({ client, userId, outboxStore, readFile, upload, hostedUpload, newId, persistFile, releasePreview, loadPeople, videoUpload, attachmentKinds, children, }: ChatProviderProps): react.JSX.Element;
106
132
  declare function useChat(): ChatContextValue;
107
133
  /** Your conversations, live. `kind` narrows to one kind; `archived` shows the archived ones. */
108
134
  declare function useInbox(opts?: {
@@ -142,10 +168,10 @@ declare function useConversation(conversationId: string | null | undefined, opts
142
168
  conversationLoading: boolean;
143
169
  /** Your role couldn't be loaded (offline): offer `refetchAccess`, don't say "can't post". */
144
170
  accessError: Error | null;
145
- refetchAccess: (options?: _tanstack_query_core.RefetchOptions) => Promise<_tanstack_query_core.QueryObserverResult<NoInfer<{
146
- role: ChatRole | null;
147
- canPost: boolean;
148
- }>, Error>>;
171
+ /** Retries everything this conversation loads (role, members, the conversation, topics, availability, pins), not only access. */
172
+ refetchAccess: () => Promise<undefined>;
173
+ /** Same as refetchAccess: every per-conversation lookup again, for a Retry after being offline. */
174
+ refetchConversation: () => Promise<undefined>;
149
175
  /** The kind's rules (max members, threads, who posts). */
150
176
  kind: NonNullable<NoInfer<ChatKindRules | null>> | null;
151
177
  /** May you pin here: owners and admins, or anyone in a two-person conversation. */
@@ -213,8 +239,9 @@ declare function useTopics(conversationId: string | null | undefined): {
213
239
  };
214
240
  /**
215
241
  * Your scheduled messages (in one conversation, or all), soonest first, with `schedule` and
216
- * `cancel`. `schedule` uploads byte files first (`LocalFile.data` a Blob or ArrayBuffer; a
217
- * native URI goes through the provider's `readFile`), then schedules them with the message.
242
+ * `cancel` (which also removes one that failed). `schedule` uploads its files to storage first,
243
+ * the way `send` does (the provider's `upload` hook, or a native URI read through `readFile`),
244
+ * then schedules them with the message. A file marked `hosted` goes to storage too.
218
245
  */
219
246
  declare function useScheduled(conversationId?: string | null, opts?: {
220
247
  enabled?: boolean;
@@ -276,4 +303,4 @@ declare const LINK_PREVIEW_CONCURRENCY = 4;
276
303
  */
277
304
  declare function useLinkPreviews(messageIds: (string | null | undefined)[]): Record<string, ChatLinkPreview[]>;
278
305
 
279
- export { CATCH_UP_MAX_PAGES, CATCH_UP_PAGE, type ChatAttachmentKind, type ChatPerson, ChatProvider, type ChatProviderProps, type ChatVideoUpload, type ConversationState, LINK_PREVIEW_CONCURRENCY, LocalFile, type MessagesData, QueuedMessage, ThreadItem, type UseConversationOptions, belongsTo, chatKeys, useAvailability, useBroadcast, useChat, useConversation, useDebouncedValue, useFileUrls, useInbox, useLinkPreviews, useMessageActions, usePeople, usePerson, usePins, useScheduled, useSearch, useStartConversation, useTopics, useUnreadTotal };
306
+ export { CATCH_UP_MAX_PAGES, CATCH_UP_PAGE, type ChatAttachmentKind, type ChatCardContext, type ChatPerson, ChatProvider, type ChatProviderProps, type ChatVideoUpload, type ConversationState, LINK_PREVIEW_CONCURRENCY, LocalFile, type MessagesData, QueuedMessage, ThreadItem, type UseConversationOptions, belongsTo, chatKeys, hostedUploadFrom, useAvailability, useBroadcast, useChat, useConversation, useDebouncedValue, useFileUrls, useInbox, useLinkPreviews, useMessageActions, usePeople, usePerson, usePins, useScheduled, useSearch, useStartConversation, useTopics, useUnreadTotal };
package/dist/react.js CHANGED
@@ -1,5 +1,5 @@
1
- export { CATCH_UP_MAX_PAGES, CATCH_UP_PAGE, ChatProvider, LINK_PREVIEW_CONCURRENCY, belongsTo, chatKeys, useAvailability, useBroadcast, useChat, useConversation, useDebouncedValue, useFileUrls, useInbox, useLinkPreviews, useMessageActions, usePeople, usePerson, usePins, useScheduled, useSearch, useStartConversation, useTopics, useUnreadTotal } from './chunk-PZTOY2LY.js';
2
- import './chunk-5D5A7I4Q.js';
3
- import './chunk-7S6HYNWW.js';
1
+ export { CATCH_UP_MAX_PAGES, CATCH_UP_PAGE, ChatProvider, LINK_PREVIEW_CONCURRENCY, belongsTo, chatKeys, hostedUploadFrom, useAvailability, useBroadcast, useChat, useConversation, useDebouncedValue, useFileUrls, useInbox, useLinkPreviews, useMessageActions, usePeople, usePerson, usePins, useScheduled, useSearch, useStartConversation, useTopics, useUnreadTotal } from './chunk-JU2FAPBI.js';
2
+ import './chunk-A4YJX7KT.js';
3
+ import './chunk-GBJ3YI4E.js';
4
4
  //# sourceMappingURL=react.js.map
5
5
  //# sourceMappingURL=react.js.map
package/dist/server.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { w as ChatSupabase } from './client-DMa2j7kz.js';
1
+ import { w as ChatSupabase } from './client-DUIG1hum.js';
2
2
  import '@supabase/supabase-js';
3
3
 
4
4
  /**
@@ -309,6 +309,47 @@ declare function muxVideo(options: MuxVideoOptions): VideoProvider & {
309
309
  }): Promise<string>;
310
310
  };
311
311
 
312
+ /**
313
+ * A pretend hosted video service for local development, behind `VideoProvider`, so the hosted
314
+ * path (pending attachment, upload, ready) runs without a Mux account.
315
+ *
316
+ * createUpload a local upload URL (`uploadUrl?id=<upload id>`) and its id, `fake_<uuid>`
317
+ * handler the fetch handler to mount at `uploadUrl`: takes the PUT body, stores it in
318
+ * the genesis-chat bucket, then marks the attachment ready
319
+ * markReady what Mux's `video.asset.ready` webhook does: `update_attachment` with status
320
+ * ready, a playback id (`fake-<upload id>`) and a fake HLS URL
321
+ * playbackUrl a signed link to the stored file (the fake HLS URL when nothing was stored)
322
+ *
323
+ * It refuses to be made when NODE_ENV is `production`, unless `force` is set.
324
+ */
325
+
326
+ interface FakeVideoProviderOptions {
327
+ /** A service-role client: it writes the bucket and calls `update_attachment`. */
328
+ supabase: ChatSupabase;
329
+ /** Where `handler` is mounted, e.g. `http://localhost:3000/api/chat/fake-video`. */
330
+ uploadUrl: string;
331
+ /** The `provider` value it stands in for. Default `mux`. */
332
+ name?: "mux" | "cloudflare";
333
+ /** Run even when NODE_ENV is `production`. */
334
+ force?: boolean;
335
+ /** NODE_ENV, read from `process.env` by default. */
336
+ env?: string | undefined;
337
+ /** How long `handler` waits for the message carrying the upload to arrive before answering 202 (default 10 s). */
338
+ readyWaitMs?: number;
339
+ newId?: () => string;
340
+ sleep?: (ms: number) => Promise<void>;
341
+ }
342
+ interface FakeReadyOptions {
343
+ durationMs?: number;
344
+ /** A poster image already in the genesis-chat bucket. */
345
+ posterPath?: string;
346
+ }
347
+ declare function fakeVideoProvider(options: FakeVideoProviderOptions): VideoProvider & {
348
+ markReady(providerId: string, ready?: FakeReadyOptions): Promise<void>;
349
+ handler(request: Request): Promise<Response>;
350
+ playbackUrl(playbackId: string): Promise<string>;
351
+ };
352
+
312
353
  /**
313
354
  * A plain `deliver` for apps without genesis-notifications yet: one Expo push
314
355
  * per device token, in requests of 100 (https://docs.expo.dev/push-notifications/sending-notifications/).
@@ -797,4 +838,4 @@ interface VideoUploadHandlerOptions {
797
838
  }
798
839
  declare function createVideoUploadHandler(options: VideoUploadHandlerOptions): (request: Request) => Promise<Response>;
799
840
 
800
- export { ANTHROPIC_MESSAGES_URL, ANTHROPIC_VERSION, type AnthropicLike, type AssistantMessage, type AssistantResult, CHAT_ASSISTANT_MODEL, CHAT_MAX_ATTEMPTS, CHAT_NOTIFICATION_CATEGORIES, CHAT_NOTIFICATION_TYPES, CHAT_NOTIFICATION_TYPE_ID, type ChatAssistant, type ChatAssistantOptions, type ChatDeliver, type ChatEffect, type ChatEffectKind, type ChatNotification, type ChatNotifyInput, type ChatRecipient, type ChatRunReport, type ChatRunner, type ChatRunnerHandlerOptions, type ChatRunnerOptions, type ChatUploadBinding, type DeepgramOptions, type DraftReplyInput, EXPO_BATCH, EXPO_SEND_URL, type ExpoDeliverOptions, type ExpoDeliveryTicket, type ExpoPushMessage, type ExpoPushTicket, type LinkPreview, type LinkUnfurler, type LinkUnfurlerOptions, type ModerateInput, type ModerationResult, type ModerationThresholds, type ModerationVerdict, type Moderator, type MuxSigningKey, type MuxVideoOptions, type NotificationsDeliverOptions, OPENAI_MODERATIONS_URL, OPENAI_MODERATION_CATEGORIES, type OpenAiModeratorOptions, type OpenAiTranscriberOptions, type RecapInput, type TranscribeInput, type TranscribeResult, type Transcriber, type VideoProvider, type VideoUploadHandlerOptions, carriesCredential, chatNotifyInput, chatRunnerHandler, chatUploadPassthrough, createChatAssistant, createChatRunner, createExpoDeliver, createLinkUnfurler, createVideoUploadHandler, deadTokens, decodeEntities, deepgramTranscriber, draftPrompt, guardedLookup, isPrivateAddress, moderationVerdict, muxPlaybackToken, muxPlaybackUrl, muxVideo, notificationsDeliver, openAiModerator, openAiTranscriber, parseChatUploadPassthrough, parseOpenGraph, recapPrompt, transcriptLines, verifyMuxSignature };
841
+ export { ANTHROPIC_MESSAGES_URL, ANTHROPIC_VERSION, type AnthropicLike, type AssistantMessage, type AssistantResult, CHAT_ASSISTANT_MODEL, CHAT_MAX_ATTEMPTS, CHAT_NOTIFICATION_CATEGORIES, CHAT_NOTIFICATION_TYPES, CHAT_NOTIFICATION_TYPE_ID, type ChatAssistant, type ChatAssistantOptions, type ChatDeliver, type ChatEffect, type ChatEffectKind, type ChatNotification, type ChatNotifyInput, type ChatRecipient, type ChatRunReport, type ChatRunner, type ChatRunnerHandlerOptions, type ChatRunnerOptions, type ChatUploadBinding, type DeepgramOptions, type DraftReplyInput, EXPO_BATCH, EXPO_SEND_URL, type ExpoDeliverOptions, type ExpoDeliveryTicket, type ExpoPushMessage, type ExpoPushTicket, type FakeReadyOptions, type FakeVideoProviderOptions, type LinkPreview, type LinkUnfurler, type LinkUnfurlerOptions, type ModerateInput, type ModerationResult, type ModerationThresholds, type ModerationVerdict, type Moderator, type MuxSigningKey, type MuxVideoOptions, type NotificationsDeliverOptions, OPENAI_MODERATIONS_URL, OPENAI_MODERATION_CATEGORIES, type OpenAiModeratorOptions, type OpenAiTranscriberOptions, type RecapInput, type TranscribeInput, type TranscribeResult, type Transcriber, type VideoProvider, type VideoUploadHandlerOptions, carriesCredential, chatNotifyInput, chatRunnerHandler, chatUploadPassthrough, createChatAssistant, createChatRunner, createExpoDeliver, createLinkUnfurler, createVideoUploadHandler, deadTokens, decodeEntities, deepgramTranscriber, draftPrompt, fakeVideoProvider, guardedLookup, isPrivateAddress, moderationVerdict, muxPlaybackToken, muxPlaybackUrl, muxVideo, notificationsDeliver, openAiModerator, openAiTranscriber, parseChatUploadPassthrough, parseOpenGraph, recapPrompt, transcriptLines, verifyMuxSignature };
package/dist/server.js CHANGED
@@ -1,4 +1,4 @@
1
- import { CHAT_SCHEMA, CHAT_BUCKET } from './chunk-7S6HYNWW.js';
1
+ import { CHAT_BUCKET, CHAT_SCHEMA } from './chunk-GBJ3YI4E.js';
2
2
  import { chatPushPayload } from './chunk-VEPZROQB.js';
3
3
 
4
4
  // src/server/runner.ts
@@ -321,8 +321,8 @@ function deepgramTranscriber(options) {
321
321
  signal: AbortSignal.timeout(TIMEOUT_MS)
322
322
  });
323
323
  if (!response.ok) return failed("Deepgram", response);
324
- const json3 = await response.json();
325
- const channel = json3.results?.channels?.[0];
324
+ const json4 = await response.json();
325
+ const channel = json4.results?.channels?.[0];
326
326
  const text = channel?.alternatives?.[0]?.transcript;
327
327
  if (typeof text !== "string") throw new Error("Deepgram answered without a transcript");
328
328
  return { text: text.trim(), language: channel?.detected_language ?? input.language };
@@ -364,9 +364,9 @@ function openAiTranscriber(options) {
364
364
  signal: AbortSignal.timeout(TIMEOUT_MS)
365
365
  });
366
366
  if (!response.ok) return failed("OpenAI", response);
367
- const json3 = await response.json();
368
- if (typeof json3.text !== "string") throw new Error("OpenAI answered without a transcript");
369
- return { text: json3.text.trim(), language: input.language };
367
+ const json4 = await response.json();
368
+ if (typeof json4.text !== "string") throw new Error("OpenAI answered without a transcript");
369
+ return { text: json4.text.trim(), language: input.language };
370
370
  }
371
371
  };
372
372
  }
@@ -700,6 +700,106 @@ function muxVideo(options) {
700
700
  };
701
701
  }
702
702
 
703
+ // src/server/fake-video.ts
704
+ var json2 = (status, body, headers = {}) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json", "cache-control": "no-store", ...headers } });
705
+ var EXTENSIONS2 = { "video/mp4": "mp4", "video/webm": "webm", "video/quicktime": "mov" };
706
+ var NotSentYet = class extends Error {
707
+ };
708
+ var nodeEnv = () => globalThis.process?.env?.NODE_ENV;
709
+ function fakeVideoProvider(options) {
710
+ const env = "env" in options ? options.env : nodeEnv();
711
+ if (env === "production" && !options.force) {
712
+ throw new Error("fakeVideoProvider is for development: it won't run with NODE_ENV=production (pass `force: true` to override)");
713
+ }
714
+ const name = options.name ?? "mux";
715
+ const db = () => options.supabase.schema(CHAT_SCHEMA);
716
+ const uploads = /* @__PURE__ */ new Map();
717
+ const newId = options.newId ?? (() => crypto.randomUUID());
718
+ const sleep = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
719
+ const fakeHls = (providerId) => `https://fake-video.invalid/${encodeURIComponent(providerId)}.m3u8`;
720
+ async function find(providerId) {
721
+ const { data, error } = await db().from("attachments").select("id").eq("provider", name).eq("provider_id", providerId).limit(1).maybeSingle();
722
+ if (error) throw new Error(`attachments: ${error.code ?? "error"}`);
723
+ return data ?? null;
724
+ }
725
+ async function markReady(providerId, ready = {}) {
726
+ const attachment = await find(providerId);
727
+ if (!attachment) throw new NotSentYet(`fakeVideoProvider: no ${name} attachment with provider_id ${providerId} (is its message sent?)`);
728
+ const { error } = await db().rpc("update_attachment", {
729
+ p_attachment_id: attachment.id,
730
+ p_patch: {
731
+ status: "ready",
732
+ playback_id: `fake-${providerId}`,
733
+ url: fakeHls(providerId),
734
+ transcript_status: "off",
735
+ ...typeof ready.durationMs === "number" ? { duration_ms: Math.round(ready.durationMs) } : {},
736
+ ...ready.posterPath ? { poster_path: ready.posterPath } : {}
737
+ }
738
+ });
739
+ if (error) throw new Error(`update_attachment: ${error.code ?? "error"}`);
740
+ }
741
+ return {
742
+ name,
743
+ async createUpload({ corsOrigin, passthrough }) {
744
+ const uploadId = `fake_${newId()}`;
745
+ uploads.set(uploadId, { passthrough, corsOrigin });
746
+ const url = new URL(options.uploadUrl);
747
+ url.searchParams.set("id", uploadId);
748
+ return { uploadId, url: url.toString() };
749
+ },
750
+ async remove(assetId) {
751
+ const id = assetId.replace(/^fake-/, "");
752
+ const path = uploads.get(id)?.path;
753
+ uploads.delete(id);
754
+ if (path) await options.supabase.storage.from(CHAT_BUCKET).remove([path]);
755
+ },
756
+ /** There is no webhook: `handler` (or `markReady`) does its work. */
757
+ async handleWebhook() {
758
+ return json2(200, { ignored: "the fake provider has no webhook; call markReady" });
759
+ },
760
+ markReady,
761
+ async playbackUrl(playbackId) {
762
+ const path = uploads.get(playbackId.replace(/^fake-/, ""))?.path;
763
+ if (!path) return fakeHls(playbackId.replace(/^fake-/, ""));
764
+ const { data, error } = await options.supabase.storage.from(CHAT_BUCKET).createSignedUrl(path, 3600);
765
+ if (error || !data?.signedUrl) throw new Error("fakeVideoProvider: couldn't sign the stored file");
766
+ return data.signedUrl;
767
+ },
768
+ async handler(request) {
769
+ const id = new URL(request.url).searchParams.get("id") ?? "";
770
+ const upload = uploads.get(id);
771
+ const cors = { "access-control-allow-origin": upload?.corsOrigin ?? "*", vary: "origin" };
772
+ if (request.method === "OPTIONS") {
773
+ return new Response(null, {
774
+ status: 204,
775
+ headers: { ...cors, "access-control-allow-methods": "PUT, OPTIONS", "access-control-allow-headers": "content-type, content-range", "access-control-max-age": "600" }
776
+ });
777
+ }
778
+ if (request.method !== "PUT") return json2(405, { error: "method not allowed" }, cors);
779
+ if (!upload) return json2(404, { error: "no such upload" }, cors);
780
+ const binding = parseChatUploadPassthrough(upload.passthrough);
781
+ const contentType = request.headers.get("content-type") ?? "video/mp4";
782
+ const path = `${binding?.conversationId ?? "fake-video"}/${binding?.userId ?? "fake-video"}/${id}.${EXTENSIONS2[contentType] ?? "mp4"}`;
783
+ const body = await request.arrayBuffer();
784
+ const { error } = await options.supabase.storage.from(CHAT_BUCKET).upload(path, body, { contentType, upsert: true });
785
+ if (error) return json2(502, { error: `storage: ${error.message}` }, cors);
786
+ upload.path = path;
787
+ const deadline = Date.now() + (options.readyWaitMs ?? 1e4);
788
+ for (; ; ) {
789
+ try {
790
+ await markReady(id);
791
+ return json2(200, { ok: true, providerId: id, path }, cors);
792
+ } catch (e) {
793
+ if (Date.now() >= deadline || !(e instanceof NotSentYet)) {
794
+ return json2(202, { stored: true, ready: false, providerId: id, path, error: e instanceof Error ? e.message : String(e) }, cors);
795
+ }
796
+ await sleep(500);
797
+ }
798
+ }
799
+ }
800
+ };
801
+ }
802
+
703
803
  // src/server/expo.ts
704
804
  var EXPO_SEND_URL = "https://exp.host/--/api/v2/push/send";
705
805
  var EXPO_BATCH = 100;
@@ -746,8 +846,8 @@ function createExpoDeliver(options) {
746
846
  });
747
847
  throw new Error(`Expo push answered HTTP ${response.status}`);
748
848
  }
749
- const json3 = await response.json();
750
- answer = Array.isArray(json3.data) ? json3.data : [];
849
+ const json4 = await response.json();
850
+ answer = Array.isArray(json4.data) ? json4.data : [];
751
851
  sent += chunk.length;
752
852
  } catch (e) {
753
853
  lastError = e;
@@ -770,17 +870,17 @@ function deadTokens(tickets) {
770
870
  }
771
871
 
772
872
  // src/server/handler.ts
773
- var json2 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json", "cache-control": "no-store" } });
873
+ var json3 = (status, body) => new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json", "cache-control": "no-store" } });
774
874
  function chatRunnerHandler(options) {
775
875
  return async (request) => {
776
- if (!options.secret || options.secret.length < 32) return json2(500, { error: "runner secret missing or under 32 characters" });
777
- if (request.method !== "POST" && request.method !== "GET") return json2(405, { error: "method not allowed" });
876
+ if (!options.secret || options.secret.length < 32) return json3(500, { error: "runner secret missing or under 32 characters" });
877
+ if (request.method !== "POST" && request.method !== "GET") return json3(405, { error: "method not allowed" });
778
878
  const match = /^Bearer\s+(\S+)$/i.exec(request.headers.get("authorization") ?? "");
779
- if (!match || !await safeEqual(match[1], options.secret)) return json2(401, { error: "unauthorized" });
879
+ if (!match || !await safeEqual(match[1], options.secret)) return json3(401, { error: "unauthorized" });
780
880
  try {
781
- return json2(200, await options.runner.run());
881
+ return json3(200, await options.runner.run());
782
882
  } catch (e) {
783
- return json2(500, { error: e instanceof Error ? e.message.slice(0, 200) : "run failed" });
883
+ return json3(500, { error: e instanceof Error ? e.message.slice(0, 200) : "run failed" });
784
884
  }
785
885
  };
786
886
  }
@@ -1399,6 +1499,6 @@ function createChatAssistant(options) {
1399
1499
  };
1400
1500
  }
1401
1501
 
1402
- export { ANTHROPIC_MESSAGES_URL, ANTHROPIC_VERSION, CHAT_ASSISTANT_MODEL, CHAT_MAX_ATTEMPTS, CHAT_NOTIFICATION_CATEGORIES, CHAT_NOTIFICATION_TYPES, CHAT_NOTIFICATION_TYPE_ID, EXPO_BATCH, EXPO_SEND_URL, OPENAI_MODERATIONS_URL, OPENAI_MODERATION_CATEGORIES, carriesCredential, chatNotifyInput, chatRunnerHandler, chatUploadPassthrough, createChatAssistant, createChatRunner, createExpoDeliver, createLinkUnfurler, createVideoUploadHandler, deadTokens, decodeEntities, deepgramTranscriber, draftPrompt, guardedLookup, isPrivateAddress, moderationVerdict, muxPlaybackToken, muxPlaybackUrl, muxVideo, notificationsDeliver, openAiModerator, openAiTranscriber, parseChatUploadPassthrough, parseOpenGraph, recapPrompt, transcriptLines, verifyMuxSignature };
1502
+ export { ANTHROPIC_MESSAGES_URL, ANTHROPIC_VERSION, CHAT_ASSISTANT_MODEL, CHAT_MAX_ATTEMPTS, CHAT_NOTIFICATION_CATEGORIES, CHAT_NOTIFICATION_TYPES, CHAT_NOTIFICATION_TYPE_ID, EXPO_BATCH, EXPO_SEND_URL, OPENAI_MODERATIONS_URL, OPENAI_MODERATION_CATEGORIES, carriesCredential, chatNotifyInput, chatRunnerHandler, chatUploadPassthrough, createChatAssistant, createChatRunner, createExpoDeliver, createLinkUnfurler, createVideoUploadHandler, deadTokens, decodeEntities, deepgramTranscriber, draftPrompt, fakeVideoProvider, guardedLookup, isPrivateAddress, moderationVerdict, muxPlaybackToken, muxPlaybackUrl, muxVideo, notificationsDeliver, openAiModerator, openAiTranscriber, parseChatUploadPassthrough, parseOpenGraph, recapPrompt, transcriptLines, verifyMuxSignature };
1403
1503
  //# sourceMappingURL=server.js.map
1404
1504
  //# sourceMappingURL=server.js.map