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/vite.js
ADDED
|
@@ -0,0 +1,1246 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'fs';
|
|
3
|
+
import { resolve, relative, dirname, sep, posix } from 'path';
|
|
4
|
+
|
|
5
|
+
const VIRTUAL_PREFIX = '\0live:';
|
|
6
|
+
const REGISTRY_ID = '\0live:__registry';
|
|
7
|
+
const LIVE_EXPORT_RE = /export\s+const\s+(\w+)\s*=\s*live\s*\(/g;
|
|
8
|
+
const VALIDATED_EXPORT_RE = /export\s+const\s+(\w+)\s*=\s*live\.validated\s*\(/g;
|
|
9
|
+
const STREAM_EXPORT_RE = /export\s+const\s+(\w+)\s*=\s*live\.stream\s*\(/g;
|
|
10
|
+
const GUARD_EXPORT_RE = /export\s+const\s+(_guard)\s*=\s*guard\s*\(/g;
|
|
11
|
+
const DYNAMIC_STREAM_RE = /export\s+const\s+(\w+)\s*=\s*live\.stream\s*\(\s*(?:\([^)]*\)|[a-zA-Z_$][\w$]*)\s*=>/g;
|
|
12
|
+
const CRON_EXPORT_RE = /export\s+const\s+(\w+)\s*=\s*live\.cron\s*\(/g;
|
|
13
|
+
const BINARY_EXPORT_RE = /export\s+const\s+(\w+)\s*=\s*live\.binary\s*\(/g;
|
|
14
|
+
const DERIVED_EXPORT_RE = /export\s+const\s+(\w+)\s*=\s*live\.derived\s*\(/g;
|
|
15
|
+
const ROOM_EXPORT_RE = /export\s+const\s+(\w+)\s*=\s*live\.room\s*\(/g;
|
|
16
|
+
const WEBHOOK_EXPORT_RE = /export\s+const\s+(\w+)\s*=\s*live\.webhook\s*\(/g;
|
|
17
|
+
const CHANNEL_EXPORT_RE = /export\s+const\s+(\w+)\s*=\s*live\.channel\s*\(/g;
|
|
18
|
+
const DYNAMIC_CHANNEL_RE = /export\s+const\s+(\w+)\s*=\s*live\.channel\s*\(\s*(?:\([^)]*\)|[a-zA-Z_$][\w$]*)\s*=>/g;
|
|
19
|
+
const RATE_LIMIT_EXPORT_RE = /export\s+const\s+(\w+)\s*=\s*live\.rateLimit\s*\(/g;
|
|
20
|
+
const EFFECT_EXPORT_RE = /export\s+const\s+(\w+)\s*=\s*live\.effect\s*\(/g;
|
|
21
|
+
const AGGREGATE_EXPORT_RE = /export\s+const\s+(\w+)\s*=\s*live\.aggregate\s*\(/g;
|
|
22
|
+
|
|
23
|
+
/** @type {Map<string, string>} Cache file contents to avoid redundant reads within a build cycle */
|
|
24
|
+
const _fileCache = new Map();
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Read a file with caching. Returns cached content if available.
|
|
28
|
+
* @param {string} filePath
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
function _readCached(filePath) {
|
|
32
|
+
let content = _fileCache.get(filePath);
|
|
33
|
+
if (content === undefined) {
|
|
34
|
+
content = readFileSync(filePath, 'utf-8');
|
|
35
|
+
_fileCache.set(filePath, content);
|
|
36
|
+
}
|
|
37
|
+
return content;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Vite plugin for svelte-realtime.
|
|
42
|
+
* Resolves `$live/` imports to virtual modules with auto-generated client stubs.
|
|
43
|
+
*
|
|
44
|
+
* @param {{ dir?: string }} [options]
|
|
45
|
+
* @returns {import('vite').Plugin}
|
|
46
|
+
*/
|
|
47
|
+
export default function svelteRealtime(options) {
|
|
48
|
+
const dir = options?.dir || 'src/live';
|
|
49
|
+
/** @type {string} */
|
|
50
|
+
let root = '';
|
|
51
|
+
/** @type {string} */
|
|
52
|
+
let liveDir = '';
|
|
53
|
+
/** @type {boolean} */
|
|
54
|
+
let isSsr = false;
|
|
55
|
+
/** @type {boolean} */
|
|
56
|
+
let typedImports = options?.typedImports !== false;
|
|
57
|
+
/** @type {boolean} */
|
|
58
|
+
let devtools = options?.devtools !== false;
|
|
59
|
+
/** @type {boolean} */
|
|
60
|
+
let isDev = false;
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
name: 'svelte-realtime',
|
|
64
|
+
|
|
65
|
+
configResolved(config) {
|
|
66
|
+
root = config.root;
|
|
67
|
+
liveDir = resolve(root, dir);
|
|
68
|
+
isSsr = !!config.build?.ssr;
|
|
69
|
+
isDev = config.command === 'serve';
|
|
70
|
+
_fileCache.clear();
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
buildStart() {
|
|
74
|
+
_fileCache.clear();
|
|
75
|
+
if (!existsSync(liveDir)) {
|
|
76
|
+
console.warn(
|
|
77
|
+
`[svelte-realtime] Plugin loaded but no live modules found in ${dir}/`
|
|
78
|
+
);
|
|
79
|
+
} else if (typedImports) {
|
|
80
|
+
_writeTypeDeclarations(liveDir, dir);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
resolveId(id) {
|
|
85
|
+
if (id === REGISTRY_ID) return REGISTRY_ID;
|
|
86
|
+
if (id === '/@svelte-realtime-registry') return REGISTRY_ID;
|
|
87
|
+
if (id.startsWith('$live/')) {
|
|
88
|
+
const modulePath = id.slice(6); // strip '$live/'
|
|
89
|
+
return VIRTUAL_PREFIX + modulePath;
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
},
|
|
93
|
+
|
|
94
|
+
load(id, loadOptions) {
|
|
95
|
+
const ssr = loadOptions?.ssr ?? isSsr;
|
|
96
|
+
|
|
97
|
+
// Registry module
|
|
98
|
+
if (id === REGISTRY_ID) {
|
|
99
|
+
return _generateRegistry(liveDir, dir);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// $live/ virtual module
|
|
103
|
+
if (id.startsWith(VIRTUAL_PREFIX)) {
|
|
104
|
+
const modulePath = id.slice(VIRTUAL_PREFIX.length);
|
|
105
|
+
const filePath = _resolveFile(liveDir, modulePath);
|
|
106
|
+
|
|
107
|
+
if (!filePath) {
|
|
108
|
+
this.error(`[svelte-realtime] Could not resolve $live/${modulePath} -- file not found in ${dir}/`);
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (ssr) {
|
|
113
|
+
// SSR: re-export the real server module, add .load() for streams
|
|
114
|
+
return _generateSsrStubs(filePath, modulePath);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Client: generate stubs
|
|
118
|
+
return _generateClientStubs(filePath, modulePath, dir);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return null;
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
config(config, { command }) {
|
|
125
|
+
// During SSR build, inject the registry as an additional input
|
|
126
|
+
if (command === 'build' && config.build?.ssr) {
|
|
127
|
+
config.build.rollupOptions ??= {};
|
|
128
|
+
const input = config.build.rollupOptions.input;
|
|
129
|
+
if (typeof input === 'object' && !Array.isArray(input)) {
|
|
130
|
+
input['__live-registry'] = REGISTRY_ID;
|
|
131
|
+
} else if (Array.isArray(input)) {
|
|
132
|
+
const obj = {};
|
|
133
|
+
for (let i = 0; i < input.length; i++) obj[`entry${i}`] = input[i];
|
|
134
|
+
obj['__live-registry'] = REGISTRY_ID;
|
|
135
|
+
config.build.rollupOptions.input = obj;
|
|
136
|
+
} else if (typeof input === 'string') {
|
|
137
|
+
config.build.rollupOptions.input = { index: input, '__live-registry': REGISTRY_ID };
|
|
138
|
+
} else {
|
|
139
|
+
config.build.rollupOptions.input = { '__live-registry': REGISTRY_ID };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Keep svelte-realtime imports as bare specifiers so the registry
|
|
143
|
+
// and ws-handler resolve to the same Node module instance at runtime.
|
|
144
|
+
// Without this, Vite rewrites the import to a chunk-relative path,
|
|
145
|
+
// creating a separate module instance with its own registry Map.
|
|
146
|
+
const ext = config.build.rollupOptions.external;
|
|
147
|
+
const svelteRealtimeRe = /^svelte-realtime(\/.*)?$/;
|
|
148
|
+
if (Array.isArray(ext)) {
|
|
149
|
+
ext.push(svelteRealtimeRe);
|
|
150
|
+
} else if (typeof ext === 'string' || ext instanceof RegExp || typeof ext === 'function') {
|
|
151
|
+
config.build.rollupOptions.external = [ext, svelteRealtimeRe];
|
|
152
|
+
} else {
|
|
153
|
+
config.build.rollupOptions.external = [svelteRealtimeRe];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
configureServer(server) {
|
|
159
|
+
// Inject devtools script into HTML responses (works with SvelteKit + traditional Vite)
|
|
160
|
+
if (devtools) {
|
|
161
|
+
const devtoolsScript = `<script type="module">
|
|
162
|
+
import { __devtools } from 'svelte-realtime/client';
|
|
163
|
+
if (__devtools) window.__svelte_realtime_devtools = __devtools;
|
|
164
|
+
import('svelte-realtime/devtools');
|
|
165
|
+
</script>`;
|
|
166
|
+
server.middlewares.use((_req, res, next) => {
|
|
167
|
+
const originalEnd = res.end;
|
|
168
|
+
/** @type {any} */
|
|
169
|
+
const _res = res;
|
|
170
|
+
_res.end = function (/** @type {any} */ chunk, /** @type {any} */ ...args) {
|
|
171
|
+
const contentType = res.getHeader('content-type');
|
|
172
|
+
if (typeof contentType === 'string' && contentType.includes('text/html') && chunk) {
|
|
173
|
+
const html = typeof chunk === 'string' ? chunk : chunk.toString();
|
|
174
|
+
if (html.includes('</body>')) {
|
|
175
|
+
chunk = html.replace('</body>', devtoolsScript + '</body>');
|
|
176
|
+
} else if (html.includes('</head>')) {
|
|
177
|
+
chunk = html.replace('</head>', devtoolsScript + '</head>');
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return originalEnd.call(this, chunk, ...args);
|
|
181
|
+
};
|
|
182
|
+
next();
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// On first transform, load the registry to populate server-side state
|
|
187
|
+
let registryLoaded = false;
|
|
188
|
+
const originalLoad = this.load;
|
|
189
|
+
|
|
190
|
+
server.httpServer?.once('listening', async () => {
|
|
191
|
+
if (registryLoaded) return;
|
|
192
|
+
registryLoaded = true;
|
|
193
|
+
|
|
194
|
+
if (!existsSync(liveDir)) return;
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const code = _generateRegistry(liveDir, dir);
|
|
198
|
+
// Use a temporary virtual module to load the registry
|
|
199
|
+
const tempId = '/@svelte-realtime-registry';
|
|
200
|
+
server.moduleGraph.ensureEntryFromUrl(tempId);
|
|
201
|
+
await server.ssrLoadModule(tempId).catch(() => {
|
|
202
|
+
// Fallback: try to load modules individually
|
|
203
|
+
_loadRegistryDirect(server, liveDir, dir);
|
|
204
|
+
});
|
|
205
|
+
} catch {
|
|
206
|
+
_loadRegistryDirect(server, liveDir, dir);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Watch for new or deleted files in src/live/ -- these don't trigger
|
|
211
|
+
// handleHotUpdate since they're not in the module graph yet (add) or
|
|
212
|
+
// have already been removed (unlink).
|
|
213
|
+
for (const event of ['add', 'unlink']) {
|
|
214
|
+
server.watcher.on(event, async (file) => {
|
|
215
|
+
file = file.split(sep).join('/');
|
|
216
|
+
if (!file.includes('/' + dir + '/') && !file.startsWith(dir + '/')) return;
|
|
217
|
+
if (!/\.[jt]s$/.test(file) || file.endsWith('.d.ts') || file.endsWith('.test.js') || file.endsWith('.test.ts')) return;
|
|
218
|
+
|
|
219
|
+
_fileCache.clear();
|
|
220
|
+
|
|
221
|
+
if (typedImports) {
|
|
222
|
+
_writeTypeDeclarations(liveDir, dir);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const rel = relative(liveDir, file).replace(/\\/g, '/').replace(/\.[jt]s$/, '');
|
|
226
|
+
await _hmrReloadRegistry(server, liveDir, dir, rel);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
async handleHotUpdate({ file, server }) {
|
|
232
|
+
if (!file.startsWith(liveDir)) return;
|
|
233
|
+
_fileCache.delete(file);
|
|
234
|
+
|
|
235
|
+
// Regenerate type declarations on file change
|
|
236
|
+
if (typedImports) {
|
|
237
|
+
_writeTypeDeclarations(liveDir, dir);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const rel = relative(liveDir, file)
|
|
241
|
+
.replace(/\\/g, '/')
|
|
242
|
+
.replace(/\.[jt]s$/, '');
|
|
243
|
+
|
|
244
|
+
// Server-side HMR: invalidate the changed module so Vite re-executes it
|
|
245
|
+
const ssrMods = server.moduleGraph.getModulesByFile(file);
|
|
246
|
+
if (ssrMods) {
|
|
247
|
+
for (const m of ssrMods) server.moduleGraph.invalidateModule(m);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Re-register all server handlers
|
|
251
|
+
await _hmrReloadRegistry(server, liveDir, dir, rel);
|
|
252
|
+
|
|
253
|
+
// Client-side: invalidate the virtual module so the browser picks up new stubs
|
|
254
|
+
const mod = server.moduleGraph.getModuleById(VIRTUAL_PREFIX + rel);
|
|
255
|
+
if (mod) {
|
|
256
|
+
server.moduleGraph.invalidateModule(mod);
|
|
257
|
+
return [mod];
|
|
258
|
+
}
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
transformIndexHtml() {
|
|
262
|
+
// Devtools injection handled via configureServer middleware (works with SvelteKit)
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Resolve a module path to a real file.
|
|
270
|
+
* @param {string} liveDir
|
|
271
|
+
* @param {string} modulePath
|
|
272
|
+
* @returns {string | null}
|
|
273
|
+
*/
|
|
274
|
+
function _resolveFile(liveDir, modulePath) {
|
|
275
|
+
const extensions = ['.js', '.ts', '.mjs'];
|
|
276
|
+
for (const ext of extensions) {
|
|
277
|
+
const full = resolve(liveDir, modulePath + ext);
|
|
278
|
+
// Prevent path traversal outside the live directory
|
|
279
|
+
if (!full.startsWith(liveDir + sep)) return null;
|
|
280
|
+
if (existsSync(full)) return full;
|
|
281
|
+
}
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Generate SSR stubs for a live module.
|
|
287
|
+
* Re-exports everything from the real module, and adds .load() wrappers for streams.
|
|
288
|
+
* @param {string} filePath
|
|
289
|
+
* @param {string} modulePath
|
|
290
|
+
* @returns {string}
|
|
291
|
+
*/
|
|
292
|
+
function _generateSsrStubs(filePath, modulePath) {
|
|
293
|
+
const normalized = filePath.split(sep).join(posix.sep);
|
|
294
|
+
const source = _readCached(filePath);
|
|
295
|
+
|
|
296
|
+
/** @type {string[]} */
|
|
297
|
+
const storeNames = [];
|
|
298
|
+
/** @type {Set<string>} */
|
|
299
|
+
const dynamicNames = new Set();
|
|
300
|
+
let match;
|
|
301
|
+
|
|
302
|
+
// Detect dynamic (function-returning) streams and channels
|
|
303
|
+
for (const re of [DYNAMIC_STREAM_RE, DYNAMIC_CHANNEL_RE]) {
|
|
304
|
+
re.lastIndex = 0;
|
|
305
|
+
while ((match = re.exec(source)) !== null) {
|
|
306
|
+
dynamicNames.add(match[1]);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// Collect all stream-like exports that need readable() wrappers for SSR
|
|
311
|
+
for (const re of [STREAM_EXPORT_RE, CHANNEL_EXPORT_RE, DERIVED_EXPORT_RE, AGGREGATE_EXPORT_RE]) {
|
|
312
|
+
re.lastIndex = 0;
|
|
313
|
+
while ((match = re.exec(source)) !== null) {
|
|
314
|
+
storeNames.push(match[1]);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// If no store-like exports, simple re-export
|
|
319
|
+
if (storeNames.length === 0) {
|
|
320
|
+
return `export * from '${normalized}';\n`;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Re-export non-stream exports, wrap store-like exports in readable() for SSR $ prefix support
|
|
324
|
+
const lines = [
|
|
325
|
+
`import { readable } from 'svelte/store';`,
|
|
326
|
+
`import { __directCall } from 'svelte-realtime/server';`,
|
|
327
|
+
`export * from '${normalized}';`
|
|
328
|
+
];
|
|
329
|
+
|
|
330
|
+
for (const name of storeNames) {
|
|
331
|
+
if (dynamicNames.has(name)) {
|
|
332
|
+
// Dynamic stream: return a readable store from a function so name(args) works during SSR
|
|
333
|
+
lines.push(`const _${name} = (...args) => readable(undefined);`);
|
|
334
|
+
lines.push(`_${name}.load = (platform, options) => __directCall('${modulePath}/${name}', options?.args || [], platform, options);`);
|
|
335
|
+
lines.push(`export { _${name} as ${name} };`);
|
|
336
|
+
} else {
|
|
337
|
+
// Static stream: plain readable store
|
|
338
|
+
lines.push(`const _${name} = readable(undefined);`);
|
|
339
|
+
lines.push(`_${name}.load = (platform, options) => __directCall('${modulePath}/${name}', options?.args || [], platform, options);`);
|
|
340
|
+
lines.push(`export { _${name} as ${name} };`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return lines.join('\n') + '\n';
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Generate client stubs for a live module.
|
|
349
|
+
* @param {string} filePath
|
|
350
|
+
* @param {string} modulePath
|
|
351
|
+
* @param {string} dir
|
|
352
|
+
* @returns {string}
|
|
353
|
+
*/
|
|
354
|
+
function _generateClientStubs(filePath, modulePath, dir) {
|
|
355
|
+
const source = _readCached(filePath);
|
|
356
|
+
|
|
357
|
+
/** @type {string[]} */
|
|
358
|
+
const lines = [];
|
|
359
|
+
/** @type {Set<string>} */
|
|
360
|
+
const imports = new Set();
|
|
361
|
+
/** @type {Set<string>} */
|
|
362
|
+
const exportedNames = new Set();
|
|
363
|
+
/** @type {boolean} */
|
|
364
|
+
let hasGuard = false;
|
|
365
|
+
|
|
366
|
+
// Detect live() exports
|
|
367
|
+
let match;
|
|
368
|
+
LIVE_EXPORT_RE.lastIndex = 0;
|
|
369
|
+
while ((match = LIVE_EXPORT_RE.exec(source)) !== null) {
|
|
370
|
+
const name = match[1];
|
|
371
|
+
if (!/^\w+$/.test(name)) continue;
|
|
372
|
+
exportedNames.add(name);
|
|
373
|
+
imports.add('__rpc');
|
|
374
|
+
lines.push(`export const ${name} = __rpc('${modulePath}/${name}');`);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Detect live.validated() exports (treated same as live() on the client)
|
|
378
|
+
VALIDATED_EXPORT_RE.lastIndex = 0;
|
|
379
|
+
while ((match = VALIDATED_EXPORT_RE.exec(source)) !== null) {
|
|
380
|
+
const name = match[1];
|
|
381
|
+
if (!/^\w+$/.test(name)) continue;
|
|
382
|
+
if (!exportedNames.has(name)) {
|
|
383
|
+
exportedNames.add(name);
|
|
384
|
+
imports.add('__rpc');
|
|
385
|
+
lines.push(`export const ${name} = __rpc('${modulePath}/${name}');`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Detect live.rateLimit() exports (treated same as live() on the client)
|
|
390
|
+
RATE_LIMIT_EXPORT_RE.lastIndex = 0;
|
|
391
|
+
while ((match = RATE_LIMIT_EXPORT_RE.exec(source)) !== null) {
|
|
392
|
+
const name = match[1];
|
|
393
|
+
if (!/^\w+$/.test(name)) continue;
|
|
394
|
+
if (!exportedNames.has(name)) {
|
|
395
|
+
exportedNames.add(name);
|
|
396
|
+
imports.add('__rpc');
|
|
397
|
+
lines.push(`export const ${name} = __rpc('${modulePath}/${name}');`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Detect live.stream() exports — check for dynamic vs static topic
|
|
402
|
+
/** @type {Set<string>} */
|
|
403
|
+
const dynamicStreams = new Set();
|
|
404
|
+
DYNAMIC_STREAM_RE.lastIndex = 0;
|
|
405
|
+
while ((match = DYNAMIC_STREAM_RE.exec(source)) !== null) {
|
|
406
|
+
dynamicStreams.add(match[1]);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
STREAM_EXPORT_RE.lastIndex = 0;
|
|
410
|
+
while ((match = STREAM_EXPORT_RE.exec(source)) !== null) {
|
|
411
|
+
const name = match[1];
|
|
412
|
+
if (!/^\w+$/.test(name)) continue;
|
|
413
|
+
exportedNames.add(name);
|
|
414
|
+
imports.add('__stream');
|
|
415
|
+
const streamOptions = _extractStreamOptions(source, name);
|
|
416
|
+
if (dynamicStreams.has(name)) {
|
|
417
|
+
// Dynamic topic: generate a function wrapper that passes args
|
|
418
|
+
lines.push(`export const ${name} = __stream('${modulePath}/${name}', ${JSON.stringify(streamOptions)}, true);`);
|
|
419
|
+
} else {
|
|
420
|
+
lines.push(`export const ${name} = __stream('${modulePath}/${name}', ${JSON.stringify(streamOptions)});`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Detect live.channel() exports -- treated as streams on the client
|
|
425
|
+
/** @type {Set<string>} */
|
|
426
|
+
const dynamicChannels = new Set();
|
|
427
|
+
DYNAMIC_CHANNEL_RE.lastIndex = 0;
|
|
428
|
+
while ((match = DYNAMIC_CHANNEL_RE.exec(source)) !== null) {
|
|
429
|
+
dynamicChannels.add(match[1]);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
CHANNEL_EXPORT_RE.lastIndex = 0;
|
|
433
|
+
while ((match = CHANNEL_EXPORT_RE.exec(source)) !== null) {
|
|
434
|
+
const name = match[1];
|
|
435
|
+
if (!/^\w+$/.test(name)) continue;
|
|
436
|
+
if (!exportedNames.has(name)) {
|
|
437
|
+
exportedNames.add(name);
|
|
438
|
+
imports.add('__stream');
|
|
439
|
+
const channelOpts = _extractChannelOptions(source, name);
|
|
440
|
+
if (dynamicChannels.has(name)) {
|
|
441
|
+
lines.push(`export const ${name} = __stream('${modulePath}/${name}', ${JSON.stringify(channelOpts)}, true);`);
|
|
442
|
+
} else {
|
|
443
|
+
lines.push(`export const ${name} = __stream('${modulePath}/${name}', ${JSON.stringify(channelOpts)});`);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Detect guard
|
|
449
|
+
GUARD_EXPORT_RE.lastIndex = 0;
|
|
450
|
+
if (GUARD_EXPORT_RE.exec(source) !== null) {
|
|
451
|
+
hasGuard = true;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Detect live.binary() exports
|
|
455
|
+
BINARY_EXPORT_RE.lastIndex = 0;
|
|
456
|
+
while ((match = BINARY_EXPORT_RE.exec(source)) !== null) {
|
|
457
|
+
const name = match[1];
|
|
458
|
+
if (!/^\w+$/.test(name)) continue;
|
|
459
|
+
if (!exportedNames.has(name)) {
|
|
460
|
+
exportedNames.add(name);
|
|
461
|
+
imports.add('__binaryRpc');
|
|
462
|
+
lines.push(`export const ${name} = __binaryRpc('${modulePath}/${name}');`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Detect live.derived() exports (treated as streams on the client)
|
|
467
|
+
DERIVED_EXPORT_RE.lastIndex = 0;
|
|
468
|
+
while ((match = DERIVED_EXPORT_RE.exec(source)) !== null) {
|
|
469
|
+
const name = match[1];
|
|
470
|
+
if (!/^\w+$/.test(name)) continue;
|
|
471
|
+
if (!exportedNames.has(name)) {
|
|
472
|
+
exportedNames.add(name);
|
|
473
|
+
imports.add('__stream');
|
|
474
|
+
// Derived streams use 'set' merge by default
|
|
475
|
+
lines.push(`export const ${name} = __stream('${modulePath}/${name}', ${JSON.stringify({ merge: 'set', key: 'id' })});`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Detect live.room() exports -- generates data stream + presence stream + cursor stream + actions
|
|
480
|
+
ROOM_EXPORT_RE.lastIndex = 0;
|
|
481
|
+
while ((match = ROOM_EXPORT_RE.exec(source)) !== null) {
|
|
482
|
+
const name = match[1];
|
|
483
|
+
if (!/^\w+$/.test(name)) continue;
|
|
484
|
+
if (!exportedNames.has(name)) {
|
|
485
|
+
exportedNames.add(name);
|
|
486
|
+
imports.add('__stream');
|
|
487
|
+
imports.add('__rpc');
|
|
488
|
+
// Room generates a namespace object with data, presence, cursors, and actions
|
|
489
|
+
// Extract room config to determine which sub-streams exist
|
|
490
|
+
const roomInfo = _extractRoomInfo(source, name);
|
|
491
|
+
const roomLines = [];
|
|
492
|
+
roomLines.push(`export const ${name} = {`);
|
|
493
|
+
roomLines.push(` data: __stream('${modulePath}/${name}/__data', ${JSON.stringify(roomInfo.dataOpts)}, true),`);
|
|
494
|
+
if (roomInfo.hasPresence) {
|
|
495
|
+
roomLines.push(` presence: __stream('${modulePath}/${name}/__presence', ${JSON.stringify({ merge: 'presence', key: 'key' })}, true),`);
|
|
496
|
+
}
|
|
497
|
+
if (roomInfo.hasCursors) {
|
|
498
|
+
roomLines.push(` cursors: __stream('${modulePath}/${name}/__cursors', ${JSON.stringify({ merge: 'cursor', key: 'key' })}, true),`);
|
|
499
|
+
}
|
|
500
|
+
// Actions are RPCs
|
|
501
|
+
for (const action of roomInfo.actions) {
|
|
502
|
+
roomLines.push(` ${action}: __rpc('${modulePath}/${name}/__action/${action}'),`);
|
|
503
|
+
}
|
|
504
|
+
roomLines.push(`};`);
|
|
505
|
+
lines.push(roomLines.join('\n'));
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Mark webhook exports as known (server-only, no client stub)
|
|
510
|
+
WEBHOOK_EXPORT_RE.lastIndex = 0;
|
|
511
|
+
while ((match = WEBHOOK_EXPORT_RE.exec(source)) !== null) {
|
|
512
|
+
exportedNames.add(match[1]);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
// Mark cron exports as known (they are server-only, no client stub needed)
|
|
516
|
+
CRON_EXPORT_RE.lastIndex = 0;
|
|
517
|
+
while ((match = CRON_EXPORT_RE.exec(source)) !== null) {
|
|
518
|
+
exportedNames.add(match[1]);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Mark effect exports as known (server-only, no client stub needed)
|
|
522
|
+
EFFECT_EXPORT_RE.lastIndex = 0;
|
|
523
|
+
while ((match = EFFECT_EXPORT_RE.exec(source)) !== null) {
|
|
524
|
+
exportedNames.add(match[1]);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Detect live.aggregate() exports (treated as streams on the client)
|
|
528
|
+
AGGREGATE_EXPORT_RE.lastIndex = 0;
|
|
529
|
+
while ((match = AGGREGATE_EXPORT_RE.exec(source)) !== null) {
|
|
530
|
+
const name = match[1];
|
|
531
|
+
if (!/^\w+$/.test(name)) continue;
|
|
532
|
+
if (!exportedNames.has(name)) {
|
|
533
|
+
exportedNames.add(name);
|
|
534
|
+
imports.add('__stream');
|
|
535
|
+
lines.push(`export const ${name} = __stream('${modulePath}/${name}', ${JSON.stringify({ merge: 'set', key: 'id' })});`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Dev warnings for non-live exports
|
|
540
|
+
const allExportRe = /export\s+(?:const|function|let|var|class)\s+(\w+)/g;
|
|
541
|
+
allExportRe.lastIndex = 0;
|
|
542
|
+
while ((match = allExportRe.exec(source)) !== null) {
|
|
543
|
+
const name = match[1];
|
|
544
|
+
if (name === '_guard' || exportedNames.has(name)) continue;
|
|
545
|
+
if (name.startsWith('_')) {
|
|
546
|
+
// Reserved names starting with _ (except _guard)
|
|
547
|
+
console.warn(
|
|
548
|
+
`[svelte-realtime] ${dir}/${modulePath} exports '${name}' starting with _ -- reserved for internal use`
|
|
549
|
+
);
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
console.warn(
|
|
553
|
+
`[svelte-realtime] ${dir}/${modulePath} exports '${name}' which is not wrapped in live() -- it won't be callable from the client. Did you forget live()?`
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if (exportedNames.size === 0 && !hasGuard) {
|
|
558
|
+
console.warn(
|
|
559
|
+
`[svelte-realtime] ${dir}/${modulePath} has no live() or live.stream() exports`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const importLine = imports.size > 0
|
|
564
|
+
? `import { ${[...imports].join(', ')} } from 'svelte-realtime/client';\n`
|
|
565
|
+
: '';
|
|
566
|
+
|
|
567
|
+
return importLine + lines.join('\n') + '\n';
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Extract stream options from source code.
|
|
572
|
+
* @param {string} source
|
|
573
|
+
* @param {string} name
|
|
574
|
+
* @returns {{ merge?: string, key?: string, prepend?: boolean, max?: number }}
|
|
575
|
+
*/
|
|
576
|
+
function _extractStreamOptions(source, name) {
|
|
577
|
+
// Find the live.stream() call for this export
|
|
578
|
+
const pattern = new RegExp(
|
|
579
|
+
`export\\s+const\\s+${name}\\s*=\\s*live\\.stream\\s*\\(\\s*(['"\`])([^'"\`]+)\\1`,
|
|
580
|
+
's'
|
|
581
|
+
);
|
|
582
|
+
const match = pattern.exec(source);
|
|
583
|
+
|
|
584
|
+
/** @type {any} */
|
|
585
|
+
const opts = { merge: 'crud', key: 'id' };
|
|
586
|
+
|
|
587
|
+
if (!match) return opts;
|
|
588
|
+
|
|
589
|
+
// Try to extract the options object (3rd argument)
|
|
590
|
+
// Find the closing of live.stream( ... )
|
|
591
|
+
const startIdx = /** @type {number} */ (match.index) + match[0].length;
|
|
592
|
+
const rest = source.slice(startIdx);
|
|
593
|
+
|
|
594
|
+
// Look for options object like { merge: 'crud', key: 'id', prepend: true }
|
|
595
|
+
const optMatch = rest.match(/,\s*\{([^}]+)\}/);
|
|
596
|
+
if (optMatch) {
|
|
597
|
+
const optStr = optMatch[1];
|
|
598
|
+
|
|
599
|
+
const mergeMatch = optStr.match(/merge\s*:\s*['"](\w+)['"]/);
|
|
600
|
+
if (mergeMatch) opts.merge = mergeMatch[1];
|
|
601
|
+
|
|
602
|
+
const keyMatch = optStr.match(/key\s*:\s*['"](\w+)['"]/);
|
|
603
|
+
if (keyMatch) opts.key = keyMatch[1];
|
|
604
|
+
|
|
605
|
+
const prependMatch = optStr.match(/prepend\s*:\s*(true|false)/);
|
|
606
|
+
if (prependMatch) opts.prepend = prependMatch[1] === 'true';
|
|
607
|
+
|
|
608
|
+
const maxMatch = optStr.match(/max\s*:\s*(\d+)/);
|
|
609
|
+
if (maxMatch) opts.max = parseInt(maxMatch[1], 10);
|
|
610
|
+
|
|
611
|
+
const replayMatch = optStr.match(/replay\s*:\s*(true|false|\{[^}]*\})/);
|
|
612
|
+
if (replayMatch && replayMatch[1] !== 'false') opts.replay = true;
|
|
613
|
+
|
|
614
|
+
const versionMatch = optStr.match(/version\s*:\s*(\d+)/);
|
|
615
|
+
if (versionMatch) opts.version = parseInt(versionMatch[1], 10);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return opts;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Extract channel options from source code.
|
|
623
|
+
* @param {string} source
|
|
624
|
+
* @param {string} name
|
|
625
|
+
* @returns {{ merge?: string, key?: string, max?: number }}
|
|
626
|
+
*/
|
|
627
|
+
function _extractChannelOptions(source, name) {
|
|
628
|
+
/** @type {any} */
|
|
629
|
+
const opts = { merge: 'set', key: 'id' };
|
|
630
|
+
|
|
631
|
+
// Look for the options object in live.channel(topic, { ... })
|
|
632
|
+
const pattern = new RegExp(
|
|
633
|
+
`export\\s+const\\s+${name}\\s*=\\s*live\\.channel\\s*\\(`,
|
|
634
|
+
's'
|
|
635
|
+
);
|
|
636
|
+
const match = pattern.exec(source);
|
|
637
|
+
if (!match) return opts;
|
|
638
|
+
|
|
639
|
+
const startIdx = match.index + match[0].length;
|
|
640
|
+
const rest = source.slice(startIdx);
|
|
641
|
+
|
|
642
|
+
const optMatch = rest.match(/,\s*\{([^}]+)\}/);
|
|
643
|
+
if (optMatch) {
|
|
644
|
+
const optStr = optMatch[1];
|
|
645
|
+
const mergeMatch = optStr.match(/merge\s*:\s*['"](\w+)['"]/);
|
|
646
|
+
if (mergeMatch) opts.merge = mergeMatch[1];
|
|
647
|
+
const keyMatch = optStr.match(/key\s*:\s*['"](\w+)['"]/);
|
|
648
|
+
if (keyMatch) opts.key = keyMatch[1];
|
|
649
|
+
const maxMatch = optStr.match(/max\s*:\s*(\d+)/);
|
|
650
|
+
if (maxMatch) opts.max = parseInt(maxMatch[1], 10);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return opts;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Extract room configuration info from source code for client stub generation.
|
|
658
|
+
* @param {string} source
|
|
659
|
+
* @param {string} name
|
|
660
|
+
* @returns {{ dataOpts: any, hasPresence: boolean, hasCursors: boolean, actions: string[] }}
|
|
661
|
+
*/
|
|
662
|
+
function _extractRoomInfo(source, name) {
|
|
663
|
+
const info = { dataOpts: { merge: 'crud', key: 'id' }, hasPresence: false, hasCursors: false, actions: [] };
|
|
664
|
+
|
|
665
|
+
// Find the start of live.room({ ... }) call
|
|
666
|
+
const startPattern = new RegExp(
|
|
667
|
+
`export\\s+const\\s+${name}\\s*=\\s*live\\.room\\s*\\(`
|
|
668
|
+
);
|
|
669
|
+
const startMatch = startPattern.exec(source);
|
|
670
|
+
if (!startMatch) return info;
|
|
671
|
+
|
|
672
|
+
// Extract the config object body using brace matching
|
|
673
|
+
const afterOpen = source.slice(startMatch.index + startMatch[0].length);
|
|
674
|
+
const body = _extractBraceContent(afterOpen);
|
|
675
|
+
if (!body) return info;
|
|
676
|
+
|
|
677
|
+
// Check for presence
|
|
678
|
+
if (/presence\s*:/.test(body)) info.hasPresence = true;
|
|
679
|
+
// Check for cursors
|
|
680
|
+
if (/cursors\s*:/.test(body)) info.hasCursors = true;
|
|
681
|
+
|
|
682
|
+
// Extract merge mode
|
|
683
|
+
const mergeMatch = body.match(/merge\s*:\s*['"](\w+)['"]/);
|
|
684
|
+
if (mergeMatch) info.dataOpts.merge = mergeMatch[1];
|
|
685
|
+
|
|
686
|
+
// Extract key field
|
|
687
|
+
const keyMatch = body.match(/key\s*:\s*['"](\w+)['"]/);
|
|
688
|
+
if (keyMatch) info.dataOpts.key = keyMatch[1];
|
|
689
|
+
|
|
690
|
+
// Extract action names from actions: { ... }
|
|
691
|
+
const actionsIdx = body.search(/actions\s*:\s*\{/);
|
|
692
|
+
if (actionsIdx !== -1) {
|
|
693
|
+
const afterActions = body.slice(body.indexOf('{', actionsIdx));
|
|
694
|
+
const actionsBody = _extractBraceContent(afterActions);
|
|
695
|
+
if (actionsBody) {
|
|
696
|
+
// Match property names: "name:" or "async name(" patterns
|
|
697
|
+
const actionPattern = /(?:async\s+)?(\w+)\s*(?:\(|:)/g;
|
|
698
|
+
let m;
|
|
699
|
+
while ((m = actionPattern.exec(actionsBody)) !== null) {
|
|
700
|
+
const actionName = m[1];
|
|
701
|
+
if (actionName !== 'async') info.actions.push(actionName);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return info;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Extract content between matching braces { ... } respecting nesting.
|
|
711
|
+
* Input should start at or before the opening brace.
|
|
712
|
+
* @param {string} str
|
|
713
|
+
* @returns {string | null}
|
|
714
|
+
*/
|
|
715
|
+
function _extractBraceContent(str) {
|
|
716
|
+
const start = str.indexOf('{');
|
|
717
|
+
if (start === -1) return null;
|
|
718
|
+
let depth = 0;
|
|
719
|
+
for (let i = start; i < str.length; i++) {
|
|
720
|
+
if (str[i] === '{') depth++;
|
|
721
|
+
else if (str[i] === '}') {
|
|
722
|
+
depth--;
|
|
723
|
+
if (depth === 0) return str.slice(start + 1, i);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Generate the registry module that imports all live functions.
|
|
731
|
+
* @param {string} liveDir
|
|
732
|
+
* @param {string} dir
|
|
733
|
+
* @returns {string}
|
|
734
|
+
*/
|
|
735
|
+
function _generateRegistry(liveDir, dir) {
|
|
736
|
+
if (!existsSync(liveDir)) return '// No live modules found\n';
|
|
737
|
+
|
|
738
|
+
const files = _findLiveFiles(liveDir);
|
|
739
|
+
const lines = [
|
|
740
|
+
`import { __register, __registerGuard, __registerCron, __registerDerived, __registerEffect, __registerAggregate, __registerRoomActions } from 'svelte-realtime/server';`,
|
|
741
|
+
`const __L = fn => (fn.__lazy = true, fn);\n`
|
|
742
|
+
];
|
|
743
|
+
|
|
744
|
+
/** @type {Set<string>} */
|
|
745
|
+
const seenTopics = new Set();
|
|
746
|
+
|
|
747
|
+
for (const filePath of files) {
|
|
748
|
+
const rel = relative(liveDir, filePath).replace(/\\/g, '/').replace(/\.[jt]s$/, '');
|
|
749
|
+
const source = _readCached(filePath);
|
|
750
|
+
const normalizedPath = filePath.split(sep).join(posix.sep);
|
|
751
|
+
|
|
752
|
+
/** @param {string} name */
|
|
753
|
+
const _lazy = (name) => `__L(() => import('${normalizedPath}').then(m => m.${name}))`;
|
|
754
|
+
|
|
755
|
+
// Register live() exports
|
|
756
|
+
/** @type {Set<string>} */
|
|
757
|
+
const registered = new Set();
|
|
758
|
+
let match;
|
|
759
|
+
LIVE_EXPORT_RE.lastIndex = 0;
|
|
760
|
+
while ((match = LIVE_EXPORT_RE.exec(source)) !== null) {
|
|
761
|
+
const name = match[1];
|
|
762
|
+
if (!/^\w+$/.test(name)) continue;
|
|
763
|
+
registered.add(name);
|
|
764
|
+
lines.push(`__register('${rel}/${name}', ${_lazy(name)});`);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Register live.validated() exports
|
|
768
|
+
VALIDATED_EXPORT_RE.lastIndex = 0;
|
|
769
|
+
while ((match = VALIDATED_EXPORT_RE.exec(source)) !== null) {
|
|
770
|
+
const name = match[1];
|
|
771
|
+
if (!/^\w+$/.test(name)) continue;
|
|
772
|
+
if (!registered.has(name)) {
|
|
773
|
+
registered.add(name);
|
|
774
|
+
lines.push(`__register('${rel}/${name}', ${_lazy(name)});`);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// Register live.rateLimit() exports
|
|
779
|
+
RATE_LIMIT_EXPORT_RE.lastIndex = 0;
|
|
780
|
+
while ((match = RATE_LIMIT_EXPORT_RE.exec(source)) !== null) {
|
|
781
|
+
const name = match[1];
|
|
782
|
+
if (!/^\w+$/.test(name)) continue;
|
|
783
|
+
if (!registered.has(name)) {
|
|
784
|
+
registered.add(name);
|
|
785
|
+
lines.push(`__register('${rel}/${name}', ${_lazy(name)});`);
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// Register live.stream() exports
|
|
790
|
+
STREAM_EXPORT_RE.lastIndex = 0;
|
|
791
|
+
while ((match = STREAM_EXPORT_RE.exec(source)) !== null) {
|
|
792
|
+
const name = match[1];
|
|
793
|
+
if (!/^\w+$/.test(name)) continue;
|
|
794
|
+
lines.push(`__register('${rel}/${name}', ${_lazy(name)});`);
|
|
795
|
+
|
|
796
|
+
// Check for duplicate stream topics
|
|
797
|
+
const topicPattern = new RegExp(
|
|
798
|
+
`export\\s+const\\s+${name}\\s*=\\s*live\\.stream\\s*\\(\\s*['"\`]([^'"\`]+)['"\`]`
|
|
799
|
+
);
|
|
800
|
+
const topicMatch = topicPattern.exec(source);
|
|
801
|
+
if (topicMatch) {
|
|
802
|
+
const topic = topicMatch[1];
|
|
803
|
+
if (topic.startsWith('__')) {
|
|
804
|
+
console.error(
|
|
805
|
+
`[svelte-realtime] ${dir}/${rel} uses reserved topic '${topic}' -- topics starting with __ are reserved for internal use`
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
if (seenTopics.has(topic)) {
|
|
809
|
+
console.error(
|
|
810
|
+
`[svelte-realtime] Duplicate stream topic '${topic}' in ${dir}/${rel} -- each topic must be unique across all modules`
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
seenTopics.add(topic);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Register guard
|
|
818
|
+
GUARD_EXPORT_RE.lastIndex = 0;
|
|
819
|
+
if (GUARD_EXPORT_RE.exec(source) !== null) {
|
|
820
|
+
lines.push(`__registerGuard('${rel}', ${_lazy('_guard')});`);
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Register live.binary() exports
|
|
824
|
+
BINARY_EXPORT_RE.lastIndex = 0;
|
|
825
|
+
while ((match = BINARY_EXPORT_RE.exec(source)) !== null) {
|
|
826
|
+
const name = match[1];
|
|
827
|
+
if (!/^\w+$/.test(name)) continue;
|
|
828
|
+
if (!registered.has(name)) {
|
|
829
|
+
registered.add(name);
|
|
830
|
+
lines.push(`__register('${rel}/${name}', ${_lazy(name)});`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
// Register cron jobs
|
|
835
|
+
CRON_EXPORT_RE.lastIndex = 0;
|
|
836
|
+
while ((match = CRON_EXPORT_RE.exec(source)) !== null) {
|
|
837
|
+
const name = match[1];
|
|
838
|
+
if (!/^\w+$/.test(name)) continue;
|
|
839
|
+
lines.push(`__registerCron('${rel}/${name}', ${_lazy(name)});`);
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Register live.derived() exports
|
|
843
|
+
DERIVED_EXPORT_RE.lastIndex = 0;
|
|
844
|
+
while ((match = DERIVED_EXPORT_RE.exec(source)) !== null) {
|
|
845
|
+
const name = match[1];
|
|
846
|
+
if (!/^\w+$/.test(name)) continue;
|
|
847
|
+
if (!registered.has(name)) {
|
|
848
|
+
registered.add(name);
|
|
849
|
+
lines.push(`__register('${rel}/${name}', ${_lazy(name)});`);
|
|
850
|
+
lines.push(`__registerDerived('${rel}/${name}', ${_lazy(name)});`);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
// Register live.room() exports -- register sub-streams and actions lazily
|
|
855
|
+
ROOM_EXPORT_RE.lastIndex = 0;
|
|
856
|
+
while ((match = ROOM_EXPORT_RE.exec(source)) !== null) {
|
|
857
|
+
const name = match[1];
|
|
858
|
+
if (!/^\w+$/.test(name)) continue;
|
|
859
|
+
if (!registered.has(name)) {
|
|
860
|
+
registered.add(name);
|
|
861
|
+
// Register the data stream
|
|
862
|
+
lines.push(`__register('${rel}/${name}/__data', __L(() => import('${normalizedPath}').then(m => m.${name}.__dataStream)));`);
|
|
863
|
+
// Register presence stream if present
|
|
864
|
+
lines.push(`__register('${rel}/${name}/__presence', __L(() => import('${normalizedPath}').then(m => m.${name}.__presenceStream)));`);
|
|
865
|
+
// Register cursor stream if present
|
|
866
|
+
lines.push(`__register('${rel}/${name}/__cursors', __L(() => import('${normalizedPath}').then(m => m.${name}.__cursorStream)));`);
|
|
867
|
+
// Register actions (deferred -- resolved on first RPC or cron tick)
|
|
868
|
+
lines.push(`__registerRoomActions('${rel}/${name}', ${_lazy(name)});`);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
// Webhook exports are server-only (no registration needed in client registry)
|
|
873
|
+
WEBHOOK_EXPORT_RE.lastIndex = 0;
|
|
874
|
+
while ((match = WEBHOOK_EXPORT_RE.exec(source)) !== null) {
|
|
875
|
+
registered.add(match[1]);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
// Register live.channel() exports (treated like streams)
|
|
879
|
+
CHANNEL_EXPORT_RE.lastIndex = 0;
|
|
880
|
+
while ((match = CHANNEL_EXPORT_RE.exec(source)) !== null) {
|
|
881
|
+
const name = match[1];
|
|
882
|
+
if (!/^\w+$/.test(name)) continue;
|
|
883
|
+
if (!registered.has(name)) {
|
|
884
|
+
registered.add(name);
|
|
885
|
+
lines.push(`__register('${rel}/${name}', ${_lazy(name)});`);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Register live.effect() exports
|
|
890
|
+
EFFECT_EXPORT_RE.lastIndex = 0;
|
|
891
|
+
while ((match = EFFECT_EXPORT_RE.exec(source)) !== null) {
|
|
892
|
+
const name = match[1];
|
|
893
|
+
if (!/^\w+$/.test(name)) continue;
|
|
894
|
+
if (!registered.has(name)) {
|
|
895
|
+
registered.add(name);
|
|
896
|
+
lines.push(`__registerEffect('${rel}/${name}', ${_lazy(name)});`);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// Register live.aggregate() exports
|
|
901
|
+
AGGREGATE_EXPORT_RE.lastIndex = 0;
|
|
902
|
+
while ((match = AGGREGATE_EXPORT_RE.exec(source)) !== null) {
|
|
903
|
+
const name = match[1];
|
|
904
|
+
if (!/^\w+$/.test(name)) continue;
|
|
905
|
+
if (!registered.has(name)) {
|
|
906
|
+
registered.add(name);
|
|
907
|
+
lines.push(`__register('${rel}/${name}', ${_lazy(name)});`);
|
|
908
|
+
lines.push(`__registerAggregate('${rel}/${name}', ${_lazy(name)});`);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
return lines.join('\n') + '\n';
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Recursively find all .js/.ts files in the live directory.
|
|
918
|
+
* @param {string} dir
|
|
919
|
+
* @returns {string[]}
|
|
920
|
+
*/
|
|
921
|
+
function _findLiveFiles(dir) {
|
|
922
|
+
/** @type {string[]} */
|
|
923
|
+
const results = [];
|
|
924
|
+
if (!existsSync(dir)) return results;
|
|
925
|
+
|
|
926
|
+
for (const entry of readdirSync(dir)) {
|
|
927
|
+
const full = resolve(dir, entry);
|
|
928
|
+
const stat = statSync(full);
|
|
929
|
+
if (stat.isDirectory()) {
|
|
930
|
+
results.push(..._findLiveFiles(full));
|
|
931
|
+
} else if (/\.[jt]s$/.test(entry) && !entry.endsWith('.d.ts') && !entry.endsWith('.test.js') && !entry.endsWith('.test.ts')) {
|
|
932
|
+
results.push(full);
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
return results;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/**
|
|
940
|
+
* Generate and write type declarations for all $live/ modules.
|
|
941
|
+
* Creates `$types.d.ts` in the live directory with ambient module declarations.
|
|
942
|
+
* @param {string} liveDir
|
|
943
|
+
* @param {string} dir
|
|
944
|
+
*/
|
|
945
|
+
function _writeTypeDeclarations(liveDir, dir) {
|
|
946
|
+
const content = _generateTypeDeclarations(liveDir, dir);
|
|
947
|
+
if (content) {
|
|
948
|
+
writeFileSync(resolve(liveDir, '$types.d.ts'), content);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
/**
|
|
953
|
+
* Generate ambient module declarations for all $live/ modules.
|
|
954
|
+
* For TS files, attempts to extract real parameter/return types.
|
|
955
|
+
* For JS files, falls back to `any`.
|
|
956
|
+
* @param {string} liveDir
|
|
957
|
+
* @param {string} dir
|
|
958
|
+
* @returns {string}
|
|
959
|
+
*/
|
|
960
|
+
function _generateTypeDeclarations(liveDir, dir) {
|
|
961
|
+
if (!existsSync(liveDir)) return '';
|
|
962
|
+
|
|
963
|
+
const files = _findLiveFiles(liveDir);
|
|
964
|
+
if (files.length === 0) return '';
|
|
965
|
+
|
|
966
|
+
const declarations = [
|
|
967
|
+
'// Auto-generated by svelte-realtime — do not edit',
|
|
968
|
+
'// Provides client-side types for $live/ imports',
|
|
969
|
+
''
|
|
970
|
+
];
|
|
971
|
+
|
|
972
|
+
for (const filePath of files) {
|
|
973
|
+
const rel = relative(liveDir, filePath).replace(/\\/g, '/').replace(/\.[jt]s$/, '');
|
|
974
|
+
const source = _readCached(filePath);
|
|
975
|
+
const isTS = filePath.endsWith('.ts');
|
|
976
|
+
|
|
977
|
+
/** @type {string[]} */
|
|
978
|
+
const exports = [];
|
|
979
|
+
let needsReadable = false;
|
|
980
|
+
let needsRpcError = false;
|
|
981
|
+
|
|
982
|
+
// Detect live() exports
|
|
983
|
+
let match;
|
|
984
|
+
LIVE_EXPORT_RE.lastIndex = 0;
|
|
985
|
+
while ((match = LIVE_EXPORT_RE.exec(source)) !== null) {
|
|
986
|
+
const name = match[1];
|
|
987
|
+
if (isTS) {
|
|
988
|
+
const sig = _extractFunctionSignature(source, name);
|
|
989
|
+
exports.push(` export const ${name}: ${sig};`);
|
|
990
|
+
} else {
|
|
991
|
+
exports.push(` export const ${name}: (...args: any[]) => Promise<any>;`);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// Detect live.validated() exports (same as live() for types)
|
|
996
|
+
VALIDATED_EXPORT_RE.lastIndex = 0;
|
|
997
|
+
while ((match = VALIDATED_EXPORT_RE.exec(source)) !== null) {
|
|
998
|
+
const name = match[1];
|
|
999
|
+
// Only add if not already detected by LIVE_EXPORT_RE
|
|
1000
|
+
if (!exports.some(e => e.includes(`export const ${name}:`))) {
|
|
1001
|
+
exports.push(` export const ${name}: (...args: any[]) => Promise<any>;`);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Extract thrown LiveError codes for typed error unions
|
|
1006
|
+
const errorCodes = _extractErrorCodes(source);
|
|
1007
|
+
if (errorCodes.length > 0) {
|
|
1008
|
+
needsRpcError = true;
|
|
1009
|
+
const union = errorCodes.map(c => `'${c}'`).join(' | ');
|
|
1010
|
+
exports.push(` export type ErrorCode = ${union};`);
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Detect live.stream() exports
|
|
1014
|
+
STREAM_EXPORT_RE.lastIndex = 0;
|
|
1015
|
+
while ((match = STREAM_EXPORT_RE.exec(source)) !== null) {
|
|
1016
|
+
const name = match[1];
|
|
1017
|
+
needsReadable = true;
|
|
1018
|
+
needsRpcError = true;
|
|
1019
|
+
if (isTS) {
|
|
1020
|
+
const returnType = _extractStreamReturnType(source, name);
|
|
1021
|
+
exports.push(` export const ${name}: Readable<${returnType} | undefined | { error: RpcError }>;`);
|
|
1022
|
+
} else {
|
|
1023
|
+
exports.push(` export const ${name}: Readable<any>;`);
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
if (exports.length > 0) {
|
|
1028
|
+
declarations.push(`declare module '$live/${rel}' {`);
|
|
1029
|
+
if (needsReadable) {
|
|
1030
|
+
declarations.push(" import type { Readable } from 'svelte/store';");
|
|
1031
|
+
}
|
|
1032
|
+
if (needsRpcError) {
|
|
1033
|
+
declarations.push(" import type { RpcError } from 'svelte-realtime/client';");
|
|
1034
|
+
}
|
|
1035
|
+
if (needsReadable || needsRpcError) {
|
|
1036
|
+
declarations.push('');
|
|
1037
|
+
}
|
|
1038
|
+
declarations.push(...exports);
|
|
1039
|
+
declarations.push('}');
|
|
1040
|
+
declarations.push('');
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
return declarations.join('\n');
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
/**
|
|
1048
|
+
* Extract thrown LiveError codes from source for typed error unions.
|
|
1049
|
+
* Scans for `throw new LiveError('CODE', ...)` patterns.
|
|
1050
|
+
* @param {string} source
|
|
1051
|
+
* @returns {string[]}
|
|
1052
|
+
*/
|
|
1053
|
+
function _extractErrorCodes(source) {
|
|
1054
|
+
const re = /throw\s+new\s+LiveError\s*\(\s*['"](\w+)['"]/g;
|
|
1055
|
+
/** @type {Set<string>} */
|
|
1056
|
+
const codes = new Set();
|
|
1057
|
+
let m;
|
|
1058
|
+
while ((m = re.exec(source)) !== null) {
|
|
1059
|
+
codes.add(m[1]);
|
|
1060
|
+
}
|
|
1061
|
+
return [...codes].sort();
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
/**
|
|
1065
|
+
* Extract client-side function signature from a TS source, stripping the ctx first param.
|
|
1066
|
+
* Falls back to `(...args: any[]) => Promise<any>` if parsing fails.
|
|
1067
|
+
* @param {string} source
|
|
1068
|
+
* @param {string} name
|
|
1069
|
+
* @returns {string}
|
|
1070
|
+
*/
|
|
1071
|
+
function _extractFunctionSignature(source, name) {
|
|
1072
|
+
const fallback = '(...args: any[]) => Promise<any>';
|
|
1073
|
+
|
|
1074
|
+
// Match: export const name = live(async (ctx: Type, param1: T1, param2: T2): ReturnType => {
|
|
1075
|
+
// or: export const name = live(async (ctx, param1: T1) => {
|
|
1076
|
+
// Uses .+? for return type to handle nested generics like Promise<{ id: number }>
|
|
1077
|
+
const pattern = new RegExp(
|
|
1078
|
+
`export\\s+const\\s+${name}\\s*=\\s*live\\s*\\(\\s*(?:async\\s+)?` +
|
|
1079
|
+
`\\(([^)]*)\\)\\s*(?::\\s*(.+?))?\\s*=>`,
|
|
1080
|
+
's'
|
|
1081
|
+
);
|
|
1082
|
+
const match = pattern.exec(source);
|
|
1083
|
+
if (!match) return fallback;
|
|
1084
|
+
|
|
1085
|
+
const paramsStr = match[1].trim();
|
|
1086
|
+
const returnType = match[2]?.trim() || 'Promise<any>';
|
|
1087
|
+
|
|
1088
|
+
// Split params, drop the first one (ctx)
|
|
1089
|
+
const params = _splitParams(paramsStr);
|
|
1090
|
+
params.shift(); // remove ctx
|
|
1091
|
+
|
|
1092
|
+
const clientParams = params.length > 0 ? params.join(', ') : '';
|
|
1093
|
+
const clientParamsStr = clientParams ? `(${clientParams})` : '()';
|
|
1094
|
+
|
|
1095
|
+
// Normalize return type - ensure it's wrapped in Promise
|
|
1096
|
+
let normalizedReturn = returnType;
|
|
1097
|
+
if (!normalizedReturn.startsWith('Promise<')) {
|
|
1098
|
+
normalizedReturn = `Promise<${normalizedReturn}>`;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
return `${clientParamsStr} => ${normalizedReturn}`;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
/**
|
|
1105
|
+
* Extract the return type of a stream's initFn for type declarations.
|
|
1106
|
+
* Falls back to `any` if parsing fails.
|
|
1107
|
+
* @param {string} source
|
|
1108
|
+
* @param {string} name
|
|
1109
|
+
* @returns {string}
|
|
1110
|
+
*/
|
|
1111
|
+
function _extractStreamReturnType(source, name) {
|
|
1112
|
+
// Match: export const name = live.stream('topic', async (ctx): Promise<Type[]> => {
|
|
1113
|
+
// Uses balanced angle bracket matching for nested generics
|
|
1114
|
+
const pattern = new RegExp(
|
|
1115
|
+
`export\\s+const\\s+${name}\\s*=\\s*live\\.stream\\s*\\([^,]+,\\s*` +
|
|
1116
|
+
`(?:async\\s+)?\\([^)]*\\)\\s*(?::\\s*(.+?))?\\s*=>`,
|
|
1117
|
+
's'
|
|
1118
|
+
);
|
|
1119
|
+
const match = pattern.exec(source);
|
|
1120
|
+
if (!match || !match[1]) return 'any';
|
|
1121
|
+
|
|
1122
|
+
const returnAnnotation = match[1].trim();
|
|
1123
|
+
// Unwrap Promise<T> to get T
|
|
1124
|
+
const promiseMatch = returnAnnotation.match(/^Promise\s*<\s*(.+)\s*>$/s);
|
|
1125
|
+
if (promiseMatch) return promiseMatch[1].trim();
|
|
1126
|
+
return returnAnnotation;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* Split a parameter string respecting nested generics and destructuring.
|
|
1131
|
+
* @param {string} str
|
|
1132
|
+
* @returns {string[]}
|
|
1133
|
+
*/
|
|
1134
|
+
function _splitParams(str) {
|
|
1135
|
+
if (!str.trim()) return [];
|
|
1136
|
+
const params = [];
|
|
1137
|
+
let depth = 0;
|
|
1138
|
+
let current = '';
|
|
1139
|
+
for (const ch of str) {
|
|
1140
|
+
if (ch === '<' || ch === '(' || ch === '{' || ch === '[') depth++;
|
|
1141
|
+
else if (ch === '>' || ch === ')' || ch === '}' || ch === ']') depth--;
|
|
1142
|
+
if (ch === ',' && depth === 0) {
|
|
1143
|
+
params.push(current.trim());
|
|
1144
|
+
current = '';
|
|
1145
|
+
} else {
|
|
1146
|
+
current += ch;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
if (current.trim()) params.push(current.trim());
|
|
1150
|
+
return params;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* Invalidate the registry virtual module, clear all server-side registrations,
|
|
1155
|
+
* and re-import the registry so handlers are updated. If the re-import fails
|
|
1156
|
+
* (e.g. syntax error), restores the previous handlers so the server keeps working.
|
|
1157
|
+
* @param {import('vite').ViteDevServer} server
|
|
1158
|
+
* @param {string} liveDir
|
|
1159
|
+
* @param {string} dir
|
|
1160
|
+
* @param {string} rel - Relative path of the changed file (for logging)
|
|
1161
|
+
*/
|
|
1162
|
+
async function _hmrReloadRegistry(server, liveDir, dir, rel) {
|
|
1163
|
+
// Invalidate the registry virtual module so Vite regenerates it
|
|
1164
|
+
const registryMod = server.moduleGraph.getModuleById(REGISTRY_ID);
|
|
1165
|
+
if (registryMod) {
|
|
1166
|
+
server.moduleGraph.invalidateModule(registryMod);
|
|
1167
|
+
}
|
|
1168
|
+
|
|
1169
|
+
/** @type {any} */
|
|
1170
|
+
let serverMod;
|
|
1171
|
+
try {
|
|
1172
|
+
serverMod = await server.ssrLoadModule('svelte-realtime/server');
|
|
1173
|
+
} catch {
|
|
1174
|
+
console.error('[svelte-realtime] HMR failed: could not load svelte-realtime/server');
|
|
1175
|
+
return;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// Snapshot current state, then clear everything
|
|
1179
|
+
const snap = serverMod._prepareHmr();
|
|
1180
|
+
|
|
1181
|
+
try {
|
|
1182
|
+
await server.ssrLoadModule('/@svelte-realtime-registry');
|
|
1183
|
+
console.log(`[svelte-realtime] Hot-reloaded: ${dir}/${rel}`);
|
|
1184
|
+
} catch (e) {
|
|
1185
|
+
// Re-import failed -- restore old handlers so the server keeps working
|
|
1186
|
+
serverMod._restoreHmr(snap);
|
|
1187
|
+
console.error(`[svelte-realtime] HMR failed for ${dir}/${rel}:`, /** @type {Error} */ (e).message);
|
|
1188
|
+
console.error('[svelte-realtime] Previous handlers restored -- fix the error and save again');
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/**
|
|
1193
|
+
* Directly load live modules in dev mode.
|
|
1194
|
+
* @param {import('vite').ViteDevServer} server
|
|
1195
|
+
* @param {string} liveDir
|
|
1196
|
+
* @param {string} dir
|
|
1197
|
+
*/
|
|
1198
|
+
async function _loadRegistryDirect(server, liveDir, dir) {
|
|
1199
|
+
let serverMod;
|
|
1200
|
+
try {
|
|
1201
|
+
serverMod = await server.ssrLoadModule('svelte-realtime/server');
|
|
1202
|
+
} catch {
|
|
1203
|
+
console.warn('[svelte-realtime] Could not load svelte-realtime/server for direct registration');
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
const { __register, __registerGuard, __registerDerived, __registerCron, __registerEffect, __registerAggregate } = serverMod;
|
|
1208
|
+
const files = _findLiveFiles(liveDir);
|
|
1209
|
+
|
|
1210
|
+
for (const filePath of files) {
|
|
1211
|
+
try {
|
|
1212
|
+
const mod = await server.ssrLoadModule(filePath);
|
|
1213
|
+
const rel = relative(liveDir, filePath).replace(/\\/g, '/').replace(/\.[jt]s$/, '');
|
|
1214
|
+
|
|
1215
|
+
for (const [name, fn] of Object.entries(mod)) {
|
|
1216
|
+
if (name === '_guard' && /** @type {any} */ (fn)?.__isGuard) {
|
|
1217
|
+
__registerGuard(rel, fn);
|
|
1218
|
+
} else if (/** @type {any} */ (fn)?.__isRoom) {
|
|
1219
|
+
// Room: register sub-streams and actions
|
|
1220
|
+
if (fn.__dataStream) __register(rel + '/' + name + '/__data', fn.__dataStream);
|
|
1221
|
+
if (fn.__presenceStream) __register(rel + '/' + name + '/__presence', fn.__presenceStream);
|
|
1222
|
+
if (fn.__cursorStream) __register(rel + '/' + name + '/__cursors', fn.__cursorStream);
|
|
1223
|
+
if (fn.__actions) {
|
|
1224
|
+
for (const [k, v] of Object.entries(fn.__actions)) {
|
|
1225
|
+
__register(rel + '/' + name + '/__action/' + k, v);
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
} else if (/** @type {any} */ (fn)?.__isEffect) {
|
|
1229
|
+
__registerEffect(rel + '/' + name, fn);
|
|
1230
|
+
} else if (/** @type {any} */ (fn)?.__isAggregate) {
|
|
1231
|
+
__register(rel + '/' + name, fn);
|
|
1232
|
+
__registerAggregate(rel + '/' + name, fn);
|
|
1233
|
+
} else if (/** @type {any} */ (fn)?.__isDerived) {
|
|
1234
|
+
__register(rel + '/' + name, fn);
|
|
1235
|
+
__registerDerived(rel + '/' + name, fn);
|
|
1236
|
+
} else if (/** @type {any} */ (fn)?.__isCron) {
|
|
1237
|
+
__registerCron(rel + '/' + name, fn);
|
|
1238
|
+
} else if (/** @type {any} */ (fn)?.__isLive) {
|
|
1239
|
+
__register(rel + '/' + name, fn);
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
} catch (err) {
|
|
1243
|
+
console.warn(`[svelte-realtime] Failed to load ${relative(liveDir, filePath)}:`, err);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|