svelte-realtime 0.1.4
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/LICENSE +21 -0
- package/README.md +1790 -0
- package/client.d.ts +239 -0
- package/client.js +1428 -0
- package/devtools.d.ts +2 -0
- package/devtools.js +214 -0
- package/package.json +80 -0
- package/server.d.ts +815 -0
- package/server.js +2311 -0
- package/test.d.ts +110 -0
- package/test.js +330 -0
- package/vite.d.ts +43 -0
- package/vite.js +1246 -0
package/server.d.ts
ADDED
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
import type { Platform, WebSocket } from 'svelte-adapter-uws';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Context passed to every `live()` and `live.stream()` function.
|
|
5
|
+
*/
|
|
6
|
+
export interface LiveContext<UserData = unknown> {
|
|
7
|
+
/** User data attached during the WebSocket upgrade handshake. */
|
|
8
|
+
user: UserData;
|
|
9
|
+
/** The raw WebSocket connection. */
|
|
10
|
+
ws: WebSocket<UserData>;
|
|
11
|
+
/** The platform API (publish, send, topic helpers). */
|
|
12
|
+
platform: Platform;
|
|
13
|
+
/** Shorthand for `platform.publish` -- delegates to whatever platform was passed in. */
|
|
14
|
+
publish: Platform['publish'];
|
|
15
|
+
/** Cursor value sent by the client for paginated stream requests. `null` if not paginated. */
|
|
16
|
+
cursor: any;
|
|
17
|
+
/** Throttled publish -- sends at most once per `ms` milliseconds. */
|
|
18
|
+
throttle(topic: string, event: string, data: any, ms: number): void;
|
|
19
|
+
/** Debounced publish -- sends after `ms` milliseconds of silence. */
|
|
20
|
+
debounce(topic: string, event: string, data: any, ms: number): void;
|
|
21
|
+
/** Send a point-to-point signal to a specific user. */
|
|
22
|
+
signal(userId: string, event: string, data: any): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Options for `live.stream()`.
|
|
27
|
+
*/
|
|
28
|
+
export interface StreamOptions {
|
|
29
|
+
/**
|
|
30
|
+
* Merge strategy for live updates.
|
|
31
|
+
* - `'crud'` -- append/update/delete by key (default)
|
|
32
|
+
* - `'latest'` -- ring buffer of last N events
|
|
33
|
+
* - `'set'` -- replace entire value
|
|
34
|
+
* - `'presence'` -- presence join/leave tracking by key
|
|
35
|
+
* - `'cursor'` -- cursor position tracking by key
|
|
36
|
+
* @default 'crud'
|
|
37
|
+
*/
|
|
38
|
+
merge?: 'crud' | 'latest' | 'set' | 'presence' | 'cursor';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Key field for `crud` merge mode.
|
|
42
|
+
* @default 'id'
|
|
43
|
+
*/
|
|
44
|
+
key?: string;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Prepend new items instead of appending (crud mode only).
|
|
48
|
+
* @default false
|
|
49
|
+
*/
|
|
50
|
+
prepend?: boolean;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Maximum items to keep (latest mode only).
|
|
54
|
+
* @default 50
|
|
55
|
+
*/
|
|
56
|
+
max?: number;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Enable seq-based replay for gap-free reconnection.
|
|
60
|
+
* Requires the replay extension from svelte-adapter-uws-extensions.
|
|
61
|
+
* @default false
|
|
62
|
+
*/
|
|
63
|
+
replay?: boolean | { size?: number };
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Called when a client subscribes to this stream.
|
|
67
|
+
* Receives the context and resolved topic string.
|
|
68
|
+
*/
|
|
69
|
+
onSubscribe?(ctx: LiveContext, topic: string): void | Promise<void>;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Called when a client disconnects from this stream.
|
|
73
|
+
* Fires for both static and dynamic topics.
|
|
74
|
+
*/
|
|
75
|
+
onUnsubscribe?(ctx: LiveContext, topic: string): void | Promise<void>;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Subscribe-time access predicate. Checked once when a client subscribes.
|
|
79
|
+
* Return `false` to deny the subscription with an "Access denied" error.
|
|
80
|
+
* For per-event filtering, use `pipe.filter()`.
|
|
81
|
+
*/
|
|
82
|
+
filter?(ctx: LiveContext): boolean;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Subscribe-time access predicate (alias for `filter`).
|
|
86
|
+
* Use `live.access` helpers to build predicates.
|
|
87
|
+
*/
|
|
88
|
+
access?(ctx: LiveContext): boolean;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Schema version number. Increment when the data shape changes.
|
|
92
|
+
* Clients send their version on reconnect; the server applies migrations if behind.
|
|
93
|
+
*/
|
|
94
|
+
version?: number;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Migration functions keyed by the version to migrate FROM.
|
|
98
|
+
* E.g., `{ 1: (item) => ({ ...item, newField: 'default' }) }` migrates v1 to v2.
|
|
99
|
+
*/
|
|
100
|
+
migrate?: Record<number, (item: any) => any>;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Delta sync configuration for efficient reconnection.
|
|
104
|
+
* When configured, the server sends only changes since the client's last known version.
|
|
105
|
+
*/
|
|
106
|
+
delta?: {
|
|
107
|
+
/** Return the current version/hash of the data. Must be fast. */
|
|
108
|
+
version(): any | Promise<any>;
|
|
109
|
+
/** Return only the items that changed since `sinceVersion`. Return null to force full refetch. */
|
|
110
|
+
diff(sinceVersion: any): any[] | Promise<any[] | null> | null;
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Options for `handleRpc()`.
|
|
116
|
+
*/
|
|
117
|
+
export interface HandleRpcOptions {
|
|
118
|
+
/**
|
|
119
|
+
* Optional async hook that runs after the guard but before the live function.
|
|
120
|
+
* Throw `LiveError` to reject the call.
|
|
121
|
+
* Use for rate limiting, logging, metrics.
|
|
122
|
+
*/
|
|
123
|
+
beforeExecute?(ws: WebSocket<any>, rpcPath: string, args: any[]): Promise<void> | void;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Called when an RPC handler throws a non-LiveError.
|
|
127
|
+
* Use for error reporting (Sentry, logging, etc.).
|
|
128
|
+
*/
|
|
129
|
+
onError?(path: string, error: unknown, ctx: LiveContext): void;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Options for `createMessage()`.
|
|
134
|
+
*/
|
|
135
|
+
export interface CreateMessageOptions {
|
|
136
|
+
/**
|
|
137
|
+
* Transform the platform before passing to `handleRpc`.
|
|
138
|
+
* Use for wrapping with Redis pub/sub bus.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```js
|
|
142
|
+
* createMessage({ platform: (p) => bus.wrap(p) })
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
platform?(platform: Platform): Platform;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Optional async hook that runs before each RPC call.
|
|
149
|
+
* Throw `LiveError` to reject.
|
|
150
|
+
*/
|
|
151
|
+
beforeExecute?(ws: WebSocket<any>, rpcPath: string, args: any[]): Promise<void> | void;
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Called when an RPC handler throws a non-LiveError.
|
|
155
|
+
* Use for error reporting (Sentry, logging, etc.).
|
|
156
|
+
*/
|
|
157
|
+
onError?(path: string, error: unknown, ctx: LiveContext): void;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Called when a message is not an RPC request.
|
|
161
|
+
* Use for mixing RPC with custom message handling.
|
|
162
|
+
*/
|
|
163
|
+
onUnhandled?(ws: WebSocket<any>, data: ArrayBuffer, platform: Platform): void;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Mark a function as RPC-callable over WebSocket.
|
|
168
|
+
*
|
|
169
|
+
* The first argument is always `ctx: LiveContext`. Additional arguments
|
|
170
|
+
* come from the client call.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```js
|
|
174
|
+
* export const sendMessage = live(async (ctx, text) => {
|
|
175
|
+
* const msg = await db.messages.insert({ userId: ctx.user.id, text });
|
|
176
|
+
* ctx.publish('messages', 'created', msg);
|
|
177
|
+
* return msg;
|
|
178
|
+
* });
|
|
179
|
+
* ```
|
|
180
|
+
*/
|
|
181
|
+
export function live<T extends (ctx: LiveContext, ...args: any[]) => any>(fn: T): T;
|
|
182
|
+
|
|
183
|
+
export namespace live {
|
|
184
|
+
/**
|
|
185
|
+
* Mark a function as a stream provider with a static topic.
|
|
186
|
+
*
|
|
187
|
+
* @param topic - Pub/sub topic name
|
|
188
|
+
* @param initFn - Function that returns the initial data
|
|
189
|
+
* @param options - Merge strategy and options
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* ```js
|
|
193
|
+
* export const messages = live.stream('messages', async (ctx) => {
|
|
194
|
+
* return db.messages.latest(50);
|
|
195
|
+
* }, { merge: 'crud', key: 'id', prepend: true });
|
|
196
|
+
* ```
|
|
197
|
+
*/
|
|
198
|
+
function stream<T extends (ctx: LiveContext, ...args: any[]) => any>(
|
|
199
|
+
topic: string,
|
|
200
|
+
initFn: T,
|
|
201
|
+
options?: StreamOptions
|
|
202
|
+
): T;
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Mark a function as a stream provider with a dynamic topic.
|
|
206
|
+
*
|
|
207
|
+
* The topic function receives the same context and arguments as the init function,
|
|
208
|
+
* enabling per-entity streams (e.g., per-room, per-user).
|
|
209
|
+
*
|
|
210
|
+
* @param topicFn - Function that computes the topic from context and arguments
|
|
211
|
+
* @param initFn - Function that returns the initial data
|
|
212
|
+
* @param options - Merge strategy and options
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* ```js
|
|
216
|
+
* export const roomMessages = live.stream(
|
|
217
|
+
* (ctx, roomId) => 'chat:' + roomId,
|
|
218
|
+
* async (ctx, roomId) => db.messages.forRoom(roomId),
|
|
219
|
+
* { merge: 'crud', key: 'id' }
|
|
220
|
+
* );
|
|
221
|
+
* ```
|
|
222
|
+
*/
|
|
223
|
+
function stream<T extends (ctx: LiveContext, ...args: any[]) => any>(
|
|
224
|
+
topicFn: (ctx: LiveContext, ...args: any[]) => string,
|
|
225
|
+
initFn: T,
|
|
226
|
+
options?: StreamOptions
|
|
227
|
+
): T;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Create an ephemeral pub/sub channel with no database initialization.
|
|
231
|
+
* Channels have no initFn -- clients subscribe and receive live events immediately.
|
|
232
|
+
*
|
|
233
|
+
* @param topic - Static topic string
|
|
234
|
+
* @param options - Merge strategy and options
|
|
235
|
+
*
|
|
236
|
+
* @example
|
|
237
|
+
* ```js
|
|
238
|
+
* export const typing = live.channel('typing:lobby', { merge: 'presence' });
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
function channel(
|
|
242
|
+
topic: string,
|
|
243
|
+
options?: { merge?: 'crud' | 'latest' | 'set' | 'presence' | 'cursor'; key?: string; max?: number }
|
|
244
|
+
): Function;
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Create an ephemeral pub/sub channel with a dynamic topic.
|
|
248
|
+
*
|
|
249
|
+
* @param topicFn - Function that computes the topic
|
|
250
|
+
* @param options - Merge strategy and options
|
|
251
|
+
*
|
|
252
|
+
* @example
|
|
253
|
+
* ```js
|
|
254
|
+
* export const cursors = live.channel(
|
|
255
|
+
* (ctx, docId) => 'cursors:' + docId,
|
|
256
|
+
* { merge: 'cursor' }
|
|
257
|
+
* );
|
|
258
|
+
* ```
|
|
259
|
+
*/
|
|
260
|
+
function channel(
|
|
261
|
+
topicFn: (ctx: LiveContext, ...args: any[]) => string,
|
|
262
|
+
options?: { merge?: 'crud' | 'latest' | 'set' | 'presence' | 'cursor'; key?: string; max?: number }
|
|
263
|
+
): Function;
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Mark a function as a binary RPC handler.
|
|
267
|
+
* The handler receives `(ctx, buffer, ...jsonArgs)` where buffer is the raw ArrayBuffer.
|
|
268
|
+
*
|
|
269
|
+
* @param fn - Handler function (ctx, buffer, ...jsonArgs)
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```js
|
|
273
|
+
* export const uploadAvatar = live.binary(async (ctx, buffer, filename) => {
|
|
274
|
+
* await storage.put(filename, buffer);
|
|
275
|
+
* return { url: `/avatars/${filename}` };
|
|
276
|
+
* });
|
|
277
|
+
* ```
|
|
278
|
+
*/
|
|
279
|
+
function binary<T extends (ctx: LiveContext, buffer: ArrayBuffer, ...args: any[]) => any>(
|
|
280
|
+
fn: T
|
|
281
|
+
): T;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Register a global middleware that runs before per-module guards for every RPC/stream call.
|
|
285
|
+
* Middleware receives `(ctx, next)` -- call `next()` to continue the chain.
|
|
286
|
+
* Throw a LiveError to reject the call.
|
|
287
|
+
*
|
|
288
|
+
* @param fn - Middleware function
|
|
289
|
+
*
|
|
290
|
+
* @example
|
|
291
|
+
* ```js
|
|
292
|
+
* live.middleware(async (ctx, next) => {
|
|
293
|
+
* const start = Date.now();
|
|
294
|
+
* const result = await next();
|
|
295
|
+
* console.log(`RPC took ${Date.now() - start}ms`);
|
|
296
|
+
* return result;
|
|
297
|
+
* });
|
|
298
|
+
* ```
|
|
299
|
+
*/
|
|
300
|
+
function middleware(fn: (ctx: LiveContext, next: () => Promise<any>) => Promise<any>): void;
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Wrap a stream with a server-side gate predicate.
|
|
304
|
+
* If the predicate returns false, the client receives a graceful no-op
|
|
305
|
+
* (`{ data: null, gated: true }`) instead of an error.
|
|
306
|
+
*
|
|
307
|
+
* @param predicate - Synchronous function checked before subscribing
|
|
308
|
+
* @param fn - The stream function to gate
|
|
309
|
+
*
|
|
310
|
+
* @example
|
|
311
|
+
* ```js
|
|
312
|
+
* export const betaFeed = live.gate(
|
|
313
|
+
* (ctx) => ctx.user?.flags?.includes('beta'),
|
|
314
|
+
* live.stream('beta-feed', async (ctx) => db.betaFeed.latest(50))
|
|
315
|
+
* );
|
|
316
|
+
* ```
|
|
317
|
+
*/
|
|
318
|
+
function gate<T extends Function>(
|
|
319
|
+
predicate: (ctx: LiveContext, ...args: any[]) => boolean,
|
|
320
|
+
fn: T
|
|
321
|
+
): T;
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Declarative per-function rate limiting.
|
|
325
|
+
* Wraps a live() function with a sliding window rate limiter.
|
|
326
|
+
*
|
|
327
|
+
* @param config - Rate limit configuration
|
|
328
|
+
* @param fn - Handler function (ctx, ...args)
|
|
329
|
+
*
|
|
330
|
+
* @example
|
|
331
|
+
* ```js
|
|
332
|
+
* export const sendMessage = live.rateLimit({ points: 5, window: 10000 }, async (ctx, text) => {
|
|
333
|
+
* const msg = await db.messages.insert({ userId: ctx.user.id, text });
|
|
334
|
+
* ctx.publish('messages', 'created', msg);
|
|
335
|
+
* return msg;
|
|
336
|
+
* });
|
|
337
|
+
* ```
|
|
338
|
+
*/
|
|
339
|
+
function rateLimit<T extends (ctx: LiveContext, ...args: any[]) => any>(
|
|
340
|
+
config: {
|
|
341
|
+
/** Maximum number of calls allowed within the window. */
|
|
342
|
+
points: number;
|
|
343
|
+
/** Time window in milliseconds. */
|
|
344
|
+
window: number;
|
|
345
|
+
/** Custom key function. Defaults to `ctx.user.id`. */
|
|
346
|
+
key?(ctx: LiveContext): string;
|
|
347
|
+
},
|
|
348
|
+
fn: T
|
|
349
|
+
): T;
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Mark a function as RPC-callable with schema validation.
|
|
353
|
+
* Validates args[0] against the schema before calling fn.
|
|
354
|
+
* Supports Zod and Valibot schemas.
|
|
355
|
+
*
|
|
356
|
+
* @param schema - Zod or Valibot schema
|
|
357
|
+
* @param fn - Handler function (ctx, validatedInput, ...rest)
|
|
358
|
+
*
|
|
359
|
+
* @example
|
|
360
|
+
* ```js
|
|
361
|
+
* const SendSchema = z.object({ text: z.string().min(1) });
|
|
362
|
+
* export const send = live.validated(SendSchema, async (ctx, input) => {
|
|
363
|
+
* // input is validated and typed
|
|
364
|
+
* });
|
|
365
|
+
* ```
|
|
366
|
+
*/
|
|
367
|
+
function validated<S, T extends (ctx: LiveContext, input: any, ...args: any[]) => any>(
|
|
368
|
+
schema: S,
|
|
369
|
+
fn: T
|
|
370
|
+
): T;
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Create a server-side scheduled function that publishes to a topic on a cron schedule.
|
|
374
|
+
*
|
|
375
|
+
* @param schedule - Cron expression (5 fields: minute hour day month weekday)
|
|
376
|
+
* @param topic - Topic to publish results to
|
|
377
|
+
* @param fn - Async function to run on schedule
|
|
378
|
+
*
|
|
379
|
+
* @example
|
|
380
|
+
* ```js
|
|
381
|
+
* export const refreshStats = live.cron('*\/5 * * * *', 'stats', async () => {
|
|
382
|
+
* return db.stats();
|
|
383
|
+
* });
|
|
384
|
+
* ```
|
|
385
|
+
*/
|
|
386
|
+
function cron<T extends () => any>(
|
|
387
|
+
schedule: string,
|
|
388
|
+
topic: string,
|
|
389
|
+
fn: T
|
|
390
|
+
): T;
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Create a real-time incremental aggregation over a source topic.
|
|
394
|
+
*
|
|
395
|
+
* @param source - Topic to watch
|
|
396
|
+
* @param reducers - Field definitions with init/reduce/compute functions
|
|
397
|
+
* @param options - Output topic, optional snapshot, debounce
|
|
398
|
+
*
|
|
399
|
+
* @example
|
|
400
|
+
* ```js
|
|
401
|
+
* export const orderStats = live.aggregate('orders', {
|
|
402
|
+
* count: { init: () => 0, reduce: (acc, event) => event === 'created' ? acc + 1 : acc },
|
|
403
|
+
* avgValue: { compute: (state) => state.count > 0 ? state.total / state.count : 0 }
|
|
404
|
+
* }, { topic: 'order-stats' });
|
|
405
|
+
* ```
|
|
406
|
+
*/
|
|
407
|
+
function aggregate(
|
|
408
|
+
source: string,
|
|
409
|
+
reducers: Record<string, {
|
|
410
|
+
init?(): any;
|
|
411
|
+
reduce?(acc: any, event: string, data: any): any;
|
|
412
|
+
compute?(state: Record<string, any>): any;
|
|
413
|
+
}>,
|
|
414
|
+
options: {
|
|
415
|
+
topic: string;
|
|
416
|
+
snapshot?(): Promise<Record<string, any>>;
|
|
417
|
+
debounce?: number;
|
|
418
|
+
}
|
|
419
|
+
): Function;
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Create a server-side reactive side effect.
|
|
423
|
+
* Effects fire when source topics publish. Fire-and-forget -- no data, no topic.
|
|
424
|
+
*
|
|
425
|
+
* @param sources - Topic names to watch
|
|
426
|
+
* @param fn - Async function called on each matching publish
|
|
427
|
+
* @param options - Debounce settings
|
|
428
|
+
*
|
|
429
|
+
* @example
|
|
430
|
+
* ```js
|
|
431
|
+
* export const orderNotifications = live.effect(['orders'], async (event, data, platform) => {
|
|
432
|
+
* if (event === 'created') {
|
|
433
|
+
* await email.send(data.userEmail, 'Order confirmed', templates.orderConfirm(data));
|
|
434
|
+
* }
|
|
435
|
+
* });
|
|
436
|
+
* ```
|
|
437
|
+
*/
|
|
438
|
+
function effect(
|
|
439
|
+
sources: string[],
|
|
440
|
+
fn: (event: string, data: any, platform: Platform) => void | Promise<void>,
|
|
441
|
+
options?: { debounce?: number }
|
|
442
|
+
): Function;
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Create a server-side computed stream that recomputes when any source topic publishes.
|
|
446
|
+
*
|
|
447
|
+
* @param sources - Topic names to watch for changes
|
|
448
|
+
* @param fn - Async function that computes the derived value
|
|
449
|
+
* @param options - Merge mode and debounce settings
|
|
450
|
+
*
|
|
451
|
+
* @example
|
|
452
|
+
* ```js
|
|
453
|
+
* export const summary = live.derived(['orders', 'inventory'], async () => {
|
|
454
|
+
* return { totalOrders: await db.orders.count(), totalItems: await db.inventory.count() };
|
|
455
|
+
* });
|
|
456
|
+
* ```
|
|
457
|
+
*/
|
|
458
|
+
function derived<T extends () => any>(
|
|
459
|
+
sources: string[],
|
|
460
|
+
fn: T,
|
|
461
|
+
options?: { merge?: string; debounce?: number }
|
|
462
|
+
): T;
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Create a collaborative room that bundles data stream, presence, cursors, and scoped RPC.
|
|
466
|
+
*
|
|
467
|
+
* @param config - Room configuration
|
|
468
|
+
*
|
|
469
|
+
* @example
|
|
470
|
+
* ```js
|
|
471
|
+
* export const board = live.room({
|
|
472
|
+
* topic: (ctx, boardId) => 'board:' + boardId,
|
|
473
|
+
* init: async (ctx, boardId) => db.boards.get(boardId),
|
|
474
|
+
* presence: (ctx) => ({ name: ctx.user.name }),
|
|
475
|
+
* cursors: true,
|
|
476
|
+
* actions: {
|
|
477
|
+
* addCard: async (ctx, title) => { ... }
|
|
478
|
+
* }
|
|
479
|
+
* });
|
|
480
|
+
* ```
|
|
481
|
+
*/
|
|
482
|
+
function room(config: RoomConfig): RoomExport;
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* Create a webhook-to-stream bridge.
|
|
486
|
+
* The returned handler can be used in a SvelteKit +server.js POST endpoint.
|
|
487
|
+
*
|
|
488
|
+
* @param topic - Topic to publish events to
|
|
489
|
+
* @param config - Verification and transformation functions
|
|
490
|
+
*
|
|
491
|
+
* @example
|
|
492
|
+
* ```js
|
|
493
|
+
* export const stripeEvents = live.webhook('payments', {
|
|
494
|
+
* verify: ({ body, headers }) => stripe.webhooks.constructEvent(body, headers['stripe-signature'], secret),
|
|
495
|
+
* transform: (event) => ({ event: event.type, data: event.data.object })
|
|
496
|
+
* });
|
|
497
|
+
* ```
|
|
498
|
+
*/
|
|
499
|
+
function webhook(topic: string, config: WebhookConfig): WebhookHandler;
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Declarative access control helpers for subscribe-time gating.
|
|
503
|
+
* For per-event filtering, use `pipe.filter()`.
|
|
504
|
+
*/
|
|
505
|
+
const access: {
|
|
506
|
+
/** Only allow subscription if `ctx.user[field]` is present. Default field: `'id'`. */
|
|
507
|
+
owner(field?: string): (ctx: LiveContext) => boolean;
|
|
508
|
+
/** Role-based access: map role names to boolean or predicate. */
|
|
509
|
+
role(map: Record<string, true | ((ctx: LiveContext) => boolean)>): (ctx: LiveContext) => boolean;
|
|
510
|
+
/** Only allow subscription if `ctx.user.teamId` is present. */
|
|
511
|
+
team(): (ctx: LiveContext) => boolean;
|
|
512
|
+
/** OR logic: any predicate returning true allows the subscription. */
|
|
513
|
+
any(...predicates: Array<(ctx: LiveContext) => boolean>): (ctx: LiveContext) => boolean;
|
|
514
|
+
/** AND logic: all predicates must return true. */
|
|
515
|
+
all(...predicates: Array<(ctx: LiveContext) => boolean>): (ctx: LiveContext) => boolean;
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Room configuration for `live.room()`.
|
|
521
|
+
*/
|
|
522
|
+
export interface RoomConfig {
|
|
523
|
+
/** Function that computes the room topic from context and args. */
|
|
524
|
+
topic: (ctx: LiveContext, ...args: any[]) => string;
|
|
525
|
+
/** Function that returns initial data for the room. */
|
|
526
|
+
init: (ctx: LiveContext, ...args: any[]) => Promise<any>;
|
|
527
|
+
/** Function that returns presence data for the connecting user. */
|
|
528
|
+
presence?: (ctx: LiveContext) => any;
|
|
529
|
+
/** Enable cursor tracking. Pass `true` or `{ throttle: ms }`. */
|
|
530
|
+
cursors?: boolean | { throttle?: number };
|
|
531
|
+
/** Room-scoped RPC actions. */
|
|
532
|
+
actions?: Record<string, (ctx: LiveContext, ...args: any[]) => any>;
|
|
533
|
+
/** Guard function run before data access and actions. */
|
|
534
|
+
guard?: (ctx: LiveContext, ...args: any[]) => void | Promise<void>;
|
|
535
|
+
/** Called when a user joins the room. */
|
|
536
|
+
onJoin?: (ctx: LiveContext, ...args: any[]) => void | Promise<void>;
|
|
537
|
+
/** Called when a user leaves the room. */
|
|
538
|
+
onLeave?: (ctx: LiveContext) => void | Promise<void>;
|
|
539
|
+
/** Merge strategy for the data stream. @default 'crud' */
|
|
540
|
+
merge?: string;
|
|
541
|
+
/** Key field for the data stream. @default 'id' */
|
|
542
|
+
key?: string;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Return type of `live.room()`.
|
|
547
|
+
*/
|
|
548
|
+
export interface RoomExport {
|
|
549
|
+
__isRoom: true;
|
|
550
|
+
__dataStream: any;
|
|
551
|
+
__topicFn: Function;
|
|
552
|
+
__hasPresence: boolean;
|
|
553
|
+
__hasCursors: boolean;
|
|
554
|
+
__presenceStream?: any;
|
|
555
|
+
__cursorStream?: any;
|
|
556
|
+
__actions?: Record<string, any>;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Webhook configuration for `live.webhook()`.
|
|
561
|
+
*/
|
|
562
|
+
export interface WebhookConfig {
|
|
563
|
+
/** Verify the incoming request. Throw to reject. */
|
|
564
|
+
verify(req: { body: string; headers: Record<string, string> }): any;
|
|
565
|
+
/** Transform the verified event. Return null to ignore. */
|
|
566
|
+
transform(event: any): { event: string; data: any } | null;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Return type of `live.webhook()`.
|
|
571
|
+
*/
|
|
572
|
+
export interface WebhookHandler {
|
|
573
|
+
__isWebhook: true;
|
|
574
|
+
/** Handle an incoming webhook request. */
|
|
575
|
+
handle(req: { body: string; headers: Record<string, string>; platform: Platform }): Promise<{ status: number; body?: string }>;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* A stream transform step created by `pipe.filter`, `pipe.sort`, etc.
|
|
580
|
+
*/
|
|
581
|
+
export interface PipeTransform {
|
|
582
|
+
transformInit?(data: any[], ctx: LiveContext): any[] | Promise<any[]>;
|
|
583
|
+
transformEvent?(ctx: LiveContext, event: string, data: any): boolean;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Compose stream transforms that apply to initial data and live events.
|
|
588
|
+
*
|
|
589
|
+
* @param stream - The stream function to wrap
|
|
590
|
+
* @param transforms - Transform steps
|
|
591
|
+
*
|
|
592
|
+
* @example
|
|
593
|
+
* ```js
|
|
594
|
+
* export const notifications = pipe(
|
|
595
|
+
* live.stream('notifications', async (ctx) => db.notifications.all()),
|
|
596
|
+
* pipe.filter((ctx, item) => !item.dismissed),
|
|
597
|
+
* pipe.sort('createdAt', 'desc'),
|
|
598
|
+
* pipe.limit(20)
|
|
599
|
+
* );
|
|
600
|
+
* ```
|
|
601
|
+
*/
|
|
602
|
+
export function pipe<T extends Function>(stream: T, ...transforms: PipeTransform[]): T;
|
|
603
|
+
export namespace pipe {
|
|
604
|
+
/** Filter items from initial data and drop non-matching live events. */
|
|
605
|
+
function filter(predicate: (ctx: LiveContext, item: any) => boolean): PipeTransform;
|
|
606
|
+
/** Sort initial data by a field. */
|
|
607
|
+
function sort(field: string, direction?: 'asc' | 'desc'): PipeTransform;
|
|
608
|
+
/** Cap initial data to N items. */
|
|
609
|
+
function limit(n: number): PipeTransform;
|
|
610
|
+
/** Enrich each item by resolving a field via an async function. */
|
|
611
|
+
function join(field: string, resolver: (value: any) => Promise<any>, as: string): PipeTransform;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Create a per-module guard that runs before every `live()` in the same module.
|
|
616
|
+
*
|
|
617
|
+
* @example
|
|
618
|
+
* ```js
|
|
619
|
+
* export const _guard = guard((ctx) => {
|
|
620
|
+
* if (ctx.user?.role !== 'admin') throw new LiveError('FORBIDDEN', 'Admin only');
|
|
621
|
+
* });
|
|
622
|
+
* ```
|
|
623
|
+
*/
|
|
624
|
+
export function guard(
|
|
625
|
+
...fns: Array<(ctx: LiveContext) => void | Promise<void>>
|
|
626
|
+
): (ctx: LiveContext) => void | Promise<void>;
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Typed error that propagates `code` and `message` to the client.
|
|
630
|
+
* Use this for expected errors (auth failures, validation, etc.).
|
|
631
|
+
* Raw `Error` throws are caught and replaced with a generic `INTERNAL_ERROR`.
|
|
632
|
+
*/
|
|
633
|
+
export class LiveError extends Error {
|
|
634
|
+
code: string;
|
|
635
|
+
constructor(code: string, message?: string);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Check whether a raw WebSocket message is an RPC request and handle it.
|
|
640
|
+
*
|
|
641
|
+
* Returns `true` if the message was an RPC request (handled), `false` otherwise
|
|
642
|
+
* (pass through to your own logic).
|
|
643
|
+
*
|
|
644
|
+
* @param ws - The WebSocket connection
|
|
645
|
+
* @param data - Raw message data from the adapter's message hook
|
|
646
|
+
* @param platform - The platform API
|
|
647
|
+
* @param options - Optional hooks (beforeExecute)
|
|
648
|
+
*
|
|
649
|
+
* @example
|
|
650
|
+
* ```js
|
|
651
|
+
* export function message(ws, { data, platform }) {
|
|
652
|
+
* if (handleRpc(ws, data, platform)) return;
|
|
653
|
+
* // custom non-RPC handling here
|
|
654
|
+
* }
|
|
655
|
+
* ```
|
|
656
|
+
*/
|
|
657
|
+
export function handleRpc(
|
|
658
|
+
ws: WebSocket<any>,
|
|
659
|
+
data: ArrayBuffer,
|
|
660
|
+
platform: Platform,
|
|
661
|
+
options?: HandleRpcOptions
|
|
662
|
+
): boolean;
|
|
663
|
+
|
|
664
|
+
/**
|
|
665
|
+
* Ready-made message hook for zero-config RPC routing.
|
|
666
|
+
*
|
|
667
|
+
* Signature matches the adapter's `message` hook exactly.
|
|
668
|
+
* Just re-export it from your `hooks.ws.js`.
|
|
669
|
+
*
|
|
670
|
+
* @example
|
|
671
|
+
* ```js
|
|
672
|
+
* export { message } from 'svelte-realtime/server';
|
|
673
|
+
* ```
|
|
674
|
+
*/
|
|
675
|
+
export function message(
|
|
676
|
+
ws: WebSocket<any>,
|
|
677
|
+
ctx: { data: ArrayBuffer; isBinary?: boolean; platform: Platform }
|
|
678
|
+
): void;
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Create a custom message hook with options baked in.
|
|
682
|
+
*
|
|
683
|
+
* @example
|
|
684
|
+
* ```js
|
|
685
|
+
* export const message = createMessage({
|
|
686
|
+
* platform: (p) => bus.wrap(p),
|
|
687
|
+
* async beforeExecute(ws, rpcPath) {
|
|
688
|
+
* const { allowed } = await limiter.consume(ws);
|
|
689
|
+
* if (!allowed) throw new LiveError('RATE_LIMITED', 'Too many requests');
|
|
690
|
+
* }
|
|
691
|
+
* });
|
|
692
|
+
* ```
|
|
693
|
+
*/
|
|
694
|
+
export function createMessage(
|
|
695
|
+
options?: CreateMessageOptions
|
|
696
|
+
): (ws: WebSocket<any>, ctx: { data: ArrayBuffer; isBinary?: boolean; platform: Platform }) => void;
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Execute a live function directly (in-process), without WebSocket.
|
|
700
|
+
* Used by SSR load functions to call live functions server-side.
|
|
701
|
+
*
|
|
702
|
+
* @param path - RPC path (e.g. 'chat/messages')
|
|
703
|
+
* @param args - Arguments to pass (excluding ctx)
|
|
704
|
+
* @param platform - The platform API
|
|
705
|
+
* @param options - Optional user data
|
|
706
|
+
*
|
|
707
|
+
* @internal
|
|
708
|
+
*/
|
|
709
|
+
export function __directCall(
|
|
710
|
+
path: string,
|
|
711
|
+
args: any[],
|
|
712
|
+
platform: Platform,
|
|
713
|
+
options?: { user?: any }
|
|
714
|
+
): Promise<any>;
|
|
715
|
+
|
|
716
|
+
/**
|
|
717
|
+
* Capture a platform reference for cron jobs.
|
|
718
|
+
* Call this in your `open` hook if you use `live.cron()`.
|
|
719
|
+
*/
|
|
720
|
+
export function setCronPlatform(platform: Platform): void;
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Register a live function. Called by the Vite-generated registry module.
|
|
724
|
+
* @internal
|
|
725
|
+
*/
|
|
726
|
+
export function __register(path: string, fn: Function): void;
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Register a module guard. Called by the Vite-generated registry module.
|
|
730
|
+
* @internal
|
|
731
|
+
*/
|
|
732
|
+
export function __registerGuard(modulePath: string, fn: Function): void;
|
|
733
|
+
|
|
734
|
+
/**
|
|
735
|
+
* Register a cron job. Called by the Vite-generated registry module.
|
|
736
|
+
* @internal
|
|
737
|
+
*/
|
|
738
|
+
export function __registerCron(path: string, fn: Function): void;
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Register a derived stream. Called by the Vite-generated registry module.
|
|
742
|
+
* @internal
|
|
743
|
+
*/
|
|
744
|
+
export function __registerDerived(path: string, fn: Function): void;
|
|
745
|
+
|
|
746
|
+
/**
|
|
747
|
+
* Register an effect. Called by the Vite-generated registry module.
|
|
748
|
+
* @internal
|
|
749
|
+
*/
|
|
750
|
+
export function __registerEffect(path: string, fn: Function): void;
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Register an aggregate. Called by the Vite-generated registry module.
|
|
754
|
+
* @internal
|
|
755
|
+
*/
|
|
756
|
+
export function __registerAggregate(path: string, fn: Function): void;
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Register room actions lazily. Called by the Vite-generated registry module.
|
|
760
|
+
* @internal
|
|
761
|
+
*/
|
|
762
|
+
export function __registerRoomActions(basePath: string, loader: Function): void;
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Activate derived stream listeners after platform is available.
|
|
766
|
+
* Wraps `platform.publish` to detect source topic events and trigger recomputation.
|
|
767
|
+
*/
|
|
768
|
+
export function _activateDerived(platform: Platform): void;
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* Clear all cron timers and registry entries.
|
|
772
|
+
* Called during HMR to prevent orphan intervals.
|
|
773
|
+
*/
|
|
774
|
+
export function _clearCron(): void;
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Set a global error handler for cron job failures.
|
|
778
|
+
* Without this, cron errors are logged in dev and silently swallowed in production.
|
|
779
|
+
*
|
|
780
|
+
* @param handler - Receives the cron path and the thrown error
|
|
781
|
+
*
|
|
782
|
+
* @example
|
|
783
|
+
* ```js
|
|
784
|
+
* onCronError((path, error) => {
|
|
785
|
+
* sentry.captureException(error, { tags: { cron: path } });
|
|
786
|
+
* });
|
|
787
|
+
* ```
|
|
788
|
+
*/
|
|
789
|
+
export function onCronError(handler: (path: string, error: unknown) => void): void;
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Handle a WebSocket close event. Fires `onUnsubscribe` lifecycle hooks
|
|
793
|
+
* for stream functions that define them.
|
|
794
|
+
*
|
|
795
|
+
* Re-export from your `hooks.ws.js`:
|
|
796
|
+
* ```js
|
|
797
|
+
* export { close } from 'svelte-realtime/server';
|
|
798
|
+
* ```
|
|
799
|
+
*/
|
|
800
|
+
/**
|
|
801
|
+
* Subscribe a WebSocket to its user's signal topic.
|
|
802
|
+
* Call in your `open` hook to enable signal delivery.
|
|
803
|
+
*
|
|
804
|
+
* @example
|
|
805
|
+
* ```js
|
|
806
|
+
* import { enableSignals } from 'svelte-realtime/server';
|
|
807
|
+
* export function open(ws) { enableSignals(ws); }
|
|
808
|
+
* ```
|
|
809
|
+
*/
|
|
810
|
+
export function enableSignals(ws: WebSocket<any>, options?: { idField?: string }): void;
|
|
811
|
+
|
|
812
|
+
export function close(
|
|
813
|
+
ws: WebSocket<any>,
|
|
814
|
+
ctx: { platform: Platform }
|
|
815
|
+
): void;
|