lowlander 0.4.0 → 0.6.0
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/README.md +108 -78
- package/build/client/client.d.ts +8 -1
- package/build/client/client.js +44 -22
- package/build/client/client.js.map +1 -1
- package/build/dashboard/client/crud.d.ts +16 -0
- package/build/dashboard/client/crud.js +525 -0
- package/build/dashboard/client/crud.js.map +1 -0
- package/build/dashboard/client/main.d.ts +1 -0
- package/build/dashboard/client/main.js +615 -0
- package/build/dashboard/client/main.js.map +1 -0
- package/build/dashboard/client/shim-server.d.ts +3 -0
- package/build/dashboard/client/shim-server.js +2 -0
- package/build/dashboard/client/shim-server.js.map +1 -0
- package/build/dashboard/dashboard.html +20 -0
- package/build/dashboard/index.d.ts +18 -0
- package/build/dashboard/index.d.ts.map +1 -0
- package/build/dashboard/index.js +50 -0
- package/build/dashboard/index.js.map +1 -0
- package/build/dashboard/serve.d.ts +18 -0
- package/build/dashboard/serve.d.ts.map +1 -0
- package/build/dashboard/serve.js +53 -0
- package/build/dashboard/serve.js.map +1 -0
- package/build/dashboard/server.d.ts +93 -0
- package/build/dashboard/server.d.ts.map +1 -0
- package/build/dashboard/server.js +384 -0
- package/build/dashboard/server.js.map +1 -0
- package/build/examples/helloworld/.edinburgh/commit_worker.log +4927 -0
- package/build/examples/helloworld/.edinburgh/data.mdb +0 -0
- package/build/examples/helloworld/.edinburgh/lock.mdb +0 -0
- package/build/examples/helloworld/client/assets/style.css +0 -45
- package/build/examples/helloworld/client/index.html +3 -14
- package/build/examples/helloworld/client/js/base.css +1 -0
- package/build/examples/helloworld/client/js/base.d.ts +1 -4
- package/build/examples/helloworld/client/js/base.js +8 -71
- package/build/examples/helloworld/client/js/base.js.map +1 -1
- package/build/examples/helloworld/server/api.d.ts +8 -2
- package/build/examples/helloworld/server/api.d.ts.map +1 -1
- package/build/examples/helloworld/server/api.js +29 -8
- package/build/examples/helloworld/server/api.js.map +1 -1
- package/build/examples/helloworld/server/main.d.ts +1 -1
- package/build/examples/helloworld/server/main.d.ts.map +1 -1
- package/build/examples/helloworld/server/main.js +6 -17
- package/build/examples/helloworld/server/main.js.map +1 -1
- package/build/server/password.d.ts +10 -0
- package/build/server/password.d.ts.map +1 -0
- package/build/server/password.js +38 -0
- package/build/server/password.js.map +1 -0
- package/build/server/server.d.ts +5 -3
- package/build/server/server.d.ts.map +1 -1
- package/build/server/server.js +65 -7
- package/build/server/server.js.map +1 -1
- package/build/server/wshandler.d.ts +7 -1
- package/build/server/wshandler.d.ts.map +1 -1
- package/build/server/wshandler.js +54 -14
- package/build/server/wshandler.js.map +1 -1
- package/build/tsconfig.client.tsbuildinfo +1 -1
- package/build/tsconfig.server.tsbuildinfo +1 -1
- package/client/client.ts +47 -24
- package/dashboard/build-bundle.ts +44 -0
- package/dashboard/client/crud.ts +634 -0
- package/dashboard/client/index.html +12 -0
- package/dashboard/client/main.ts +554 -0
- package/dashboard/client/shim-server.ts +5 -0
- package/dashboard/index.ts +49 -0
- package/dashboard/server.ts +399 -0
- package/package.json +26 -11
- package/server/server.ts +61 -10
- package/server/wshandler.ts +57 -13
- package/skill/SKILL.md +82 -51
- package/skill/getStreamTypesForModel.md +7 -0
- package/skill/Connection_pruneCommitIds.md +0 -8
package/client/client.ts
CHANGED
|
@@ -200,9 +200,12 @@ export class Connection<T> {
|
|
|
200
200
|
private reconnectAttempts = 0;
|
|
201
201
|
/** @internal */
|
|
202
202
|
public _proxyCounter = 0;
|
|
203
|
-
private
|
|
203
|
+
private $state = A.proxy({online: false, error: undefined as undefined|string});
|
|
204
204
|
private streamCache = new Map<string, StreamCacheEntry>();
|
|
205
205
|
|
|
206
|
+
/** @internal - allows storing Connection instances in Aberdeen proxies without re-proxying */
|
|
207
|
+
get [A.OPAQUE]() { return true as const; }
|
|
208
|
+
|
|
206
209
|
/**
|
|
207
210
|
* Type-safe proxy to the server-side API. Methods return `PromiseProxy` objects
|
|
208
211
|
* that work reactively in Aberdeen scopes. `ServerProxy` returns include a
|
|
@@ -221,22 +224,36 @@ export class Connection<T> {
|
|
|
221
224
|
/**
|
|
222
225
|
* Returns the current connection status. Reactive in Aberdeen scopes.
|
|
223
226
|
*/
|
|
224
|
-
isOnline(): boolean { return this.
|
|
227
|
+
isOnline(): boolean { return this.$state.online; }
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Returns the last WebSocket error message, or `undefined` if there is none.
|
|
231
|
+
* Clears automatically when the connection comes online. Reactive in Aberdeen scopes.
|
|
232
|
+
*/
|
|
233
|
+
getError(): string | undefined { return this.$state.error; }
|
|
225
234
|
|
|
226
235
|
private connect() {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
236
|
+
let ws: WebSocket;
|
|
237
|
+
try {
|
|
238
|
+
ws = this.ws = typeof this.url === 'string'
|
|
239
|
+
? new WebSocket(this.url)
|
|
240
|
+
: this.url();
|
|
241
|
+
} catch (error: any) {
|
|
242
|
+
console.error('WebSocket connection error:', error);
|
|
243
|
+
this.$state.error = error?.message || 'WebSocket connection error';
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
230
246
|
ws.binaryType = "arraybuffer";
|
|
231
247
|
if (logLevel >= 1) console.log(`[lowlander] Connecting to WebSocket at ${typeof this.url === 'string' ? this.url : '[custom WebSocket]'}`);
|
|
232
248
|
|
|
233
249
|
ws.onopen = () => {
|
|
234
250
|
if (ws !== this.ws) return; // No longer the current connection
|
|
235
251
|
if (logLevel >= 1) console.log('[lowlander] WebSocket connected');
|
|
236
|
-
this.
|
|
252
|
+
this.$state.online = true;
|
|
253
|
+
this.$state.error = undefined;
|
|
237
254
|
this.reconnectAttempts = 0;
|
|
238
255
|
for(const request of this.activeRequests.values()) {
|
|
239
|
-
request.
|
|
256
|
+
request.$result.busy = true;
|
|
240
257
|
this.ws!.send(request.requestBuffer as Uint8Array<ArrayBuffer>);
|
|
241
258
|
}
|
|
242
259
|
};
|
|
@@ -250,6 +267,7 @@ export class Connection<T> {
|
|
|
250
267
|
ws.onerror = (error: any) => {
|
|
251
268
|
if (ws !== this.ws) return; // No longer the current connection
|
|
252
269
|
console.error('WebSocket error:', error);
|
|
270
|
+
this.$state.error = error?.message || 'WebSocket connection error';
|
|
253
271
|
this.reconnect();
|
|
254
272
|
};
|
|
255
273
|
|
|
@@ -261,7 +279,7 @@ export class Connection<T> {
|
|
|
261
279
|
|
|
262
280
|
const request = this.activeRequests.get(requestId);
|
|
263
281
|
if (!request) return; // Raced
|
|
264
|
-
const result = A.unproxy(request
|
|
282
|
+
const result = A.unproxy(request.$result);
|
|
265
283
|
|
|
266
284
|
const type = pack.read();
|
|
267
285
|
if (typeof type === 'number') {
|
|
@@ -318,12 +336,12 @@ export class Connection<T> {
|
|
|
318
336
|
} else {
|
|
319
337
|
// Create new object
|
|
320
338
|
if (prevCommitIds && commitId < (prevCommitIds.get(DEFAULT_COMMIT) ?? -1)) return; // Stale create
|
|
321
|
-
const entry = A.proxy(delta);
|
|
339
|
+
const $entry = A.proxy(delta);
|
|
322
340
|
// OPAQUE=false: excluded from A.copy recursion (so this proxy is never
|
|
323
341
|
// corrupted by being overwritten with data from a different linked model
|
|
324
342
|
// at the same array index), but still wrapped in a proxy on read.
|
|
325
|
-
(A.unproxy(entry) as any)[A.OPAQUE] = false;
|
|
326
|
-
request.database.set(dbKeyHash, entry);
|
|
343
|
+
(A.unproxy($entry) as any)[A.OPAQUE] = false;
|
|
344
|
+
request.database.set(dbKeyHash, $entry);
|
|
327
345
|
request.commitIds.set(dbKeyHash, new Map([[DEFAULT_COMMIT, commitId]]));
|
|
328
346
|
}
|
|
329
347
|
return;
|
|
@@ -332,11 +350,11 @@ export class Connection<T> {
|
|
|
332
350
|
// Each request should get one of these packet types as a response
|
|
333
351
|
if (type === SERVER_MESSAGES.error) {
|
|
334
352
|
const errorMessage = pack.readString();
|
|
335
|
-
request.
|
|
353
|
+
request.$result.error = new Error(errorMessage);
|
|
336
354
|
if (logLevel >= 2) console.log(`[lowlander] incoming error requestId=${requestId} message=${errorMessage}`);
|
|
337
355
|
|
|
338
356
|
} else if (type === SERVER_MESSAGES.response || type === SERVER_MESSAGES.response_proxy) {
|
|
339
|
-
request.
|
|
357
|
+
request.$result.value = pack.read();
|
|
340
358
|
request.virtualSocketIds = pack.read() as number[] | undefined;
|
|
341
359
|
request.hasServerProxy = type === SERVER_MESSAGES.response_proxy;
|
|
342
360
|
if (logLevel >= 2) console.log(`[lowlander] incoming response requestId=${requestId} value=${result.value} virtualSocketIds=${request.virtualSocketIds} hasServerProxy=${request.hasServerProxy}`);
|
|
@@ -349,9 +367,9 @@ export class Connection<T> {
|
|
|
349
367
|
request.hasServerProxy = type === SERVER_MESSAGES.response_proxy_model;
|
|
350
368
|
if (logLevel >= 2) console.log(`[lowlander] incoming ${request.hasServerProxy ? 'response_proxy_model' : 'response_model'} requestId=${requestId} dbKey=${dbKey} cacheMs=${cacheMs} obj=${obj}`);
|
|
351
369
|
if (obj) {
|
|
352
|
-
request.
|
|
370
|
+
request.$result.value = A.proxy(obj);
|
|
353
371
|
} else {
|
|
354
|
-
request.
|
|
372
|
+
request.$result.error = new Error('Unknown database key ' + dbKey);
|
|
355
373
|
}
|
|
356
374
|
if (cacheMs !== undefined && request.cacheKey && !this.streamCache.has(request.cacheKey)) {
|
|
357
375
|
this.streamCache.set(request.cacheKey, {
|
|
@@ -370,10 +388,10 @@ export class Connection<T> {
|
|
|
370
388
|
}
|
|
371
389
|
|
|
372
390
|
if (!request.hasServerProxy) {
|
|
373
|
-
delete (request
|
|
391
|
+
delete (request.$result as any).serverProxy;
|
|
374
392
|
}
|
|
375
393
|
|
|
376
|
-
request.
|
|
394
|
+
request.$result.busy = false;
|
|
377
395
|
|
|
378
396
|
if (request.resolve) {
|
|
379
397
|
// This does not happen on reconnect
|
|
@@ -392,7 +410,7 @@ export class Connection<T> {
|
|
|
392
410
|
private reconnect() {
|
|
393
411
|
this.ws = undefined;
|
|
394
412
|
|
|
395
|
-
this.
|
|
413
|
+
this.$state.online = false;
|
|
396
414
|
|
|
397
415
|
if (typeof this.url !== 'string') return; // No reconnect in test mode
|
|
398
416
|
|
|
@@ -438,12 +456,12 @@ export class Connection<T> {
|
|
|
438
456
|
if (cached.refCount <= 0) this.startLinger(cached);
|
|
439
457
|
});
|
|
440
458
|
|
|
441
|
-
return cached.request
|
|
459
|
+
return cached.request.$result;
|
|
442
460
|
}
|
|
443
461
|
}
|
|
444
462
|
|
|
445
463
|
const result = {busy: true} as PromiseProxy<any> & {promise: Promise<any>} & {serverProxy: any};
|
|
446
|
-
const
|
|
464
|
+
const $result = A.proxy(result);
|
|
447
465
|
|
|
448
466
|
const requestId = ++this.requestCounter;
|
|
449
467
|
|
|
@@ -463,7 +481,7 @@ export class Connection<T> {
|
|
|
463
481
|
}
|
|
464
482
|
});
|
|
465
483
|
|
|
466
|
-
const request: ActiveRequest = {
|
|
484
|
+
const request: ActiveRequest = { $result, requestBuffer: pack.toUint8Array(true), requestId, connection: this, callbacks, cacheKey };
|
|
467
485
|
result.serverProxy = new Proxy(request, proxyHandlers);
|
|
468
486
|
result.promise = new Promise((resolve, reject) => {
|
|
469
487
|
request.resolve = resolve;
|
|
@@ -489,7 +507,7 @@ export class Connection<T> {
|
|
|
489
507
|
this.cancelRequest(request);
|
|
490
508
|
});
|
|
491
509
|
|
|
492
|
-
return
|
|
510
|
+
return $result;
|
|
493
511
|
}
|
|
494
512
|
}
|
|
495
513
|
|
|
@@ -520,7 +538,12 @@ export class Connection<T> {
|
|
|
520
538
|
}
|
|
521
539
|
|
|
522
540
|
const proxyHandlers: ProxyHandler<any> = {
|
|
523
|
-
get(target: ProxyTargetType, prop: string) {
|
|
541
|
+
get(target: ProxyTargetType, prop: string | symbol) {
|
|
542
|
+
if (typeof prop === 'symbol') {
|
|
543
|
+
// Expose OPAQUE=true so Aberdeen stores us as-is without re-proxying.
|
|
544
|
+
// Also, don't dive into object args
|
|
545
|
+
return prop === A.OPAQUE ? true : prop === A.CUSTOM_DUMP ? `<ServerProxy for #${target.requestId}>` : undefined;
|
|
546
|
+
}
|
|
524
547
|
if (target.serverProxyCache?.has(prop)) {
|
|
525
548
|
return target.serverProxyCache.get(prop);
|
|
526
549
|
}
|
|
@@ -548,7 +571,7 @@ interface ProxyTargetType {
|
|
|
548
571
|
}
|
|
549
572
|
|
|
550
573
|
interface ActiveRequest extends ProxyTargetType {
|
|
551
|
-
|
|
574
|
+
$result: PromiseProxy<any>;
|
|
552
575
|
callbacks?: ((...args: any[]) => void)[];
|
|
553
576
|
virtualSocketIds?: number[];
|
|
554
577
|
database?: Map<number, Record<any,any>>; // For model streams
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Bundles dashboard/client/main.ts into build/dashboard/dashboard.html as a
|
|
3
|
+
// single self-contained file.
|
|
4
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
import { dirname, resolve } from 'path';
|
|
7
|
+
import * as esbuild from 'esbuild';
|
|
8
|
+
|
|
9
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const root = resolve(here, '..');
|
|
11
|
+
const outDir = resolve(root, 'build/dashboard');
|
|
12
|
+
mkdirSync(outDir, { recursive: true });
|
|
13
|
+
|
|
14
|
+
const debug = process.argv.includes('--debug');
|
|
15
|
+
const result = await esbuild.build({
|
|
16
|
+
entryPoints: [resolve(here, 'client/main.ts')],
|
|
17
|
+
bundle: true,
|
|
18
|
+
platform: 'browser',
|
|
19
|
+
format: 'esm',
|
|
20
|
+
minify: !debug,
|
|
21
|
+
sourcemap: debug ? 'inline' : false,
|
|
22
|
+
write: false,
|
|
23
|
+
tsconfig: resolve(root, 'tsconfig.client.json'),
|
|
24
|
+
});
|
|
25
|
+
if (debug) {
|
|
26
|
+
const dbgJs = result.outputFiles[0].text;
|
|
27
|
+
console.log('Symbol("target") occurrences (>1 means duplicate Aberdeen):',
|
|
28
|
+
(dbgJs.match(/Symbol\("target"\)/g) || []).length);
|
|
29
|
+
}
|
|
30
|
+
if (result.errors.length) {
|
|
31
|
+
for (const e of result.errors) console.error(e.text);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
const js = result.outputFiles[0].text;
|
|
35
|
+
|
|
36
|
+
const template = readFileSync(resolve(here, 'client/index.html'), 'utf8');
|
|
37
|
+
const scriptTag = `<script type="module">${js.replace(/<\/script>/gi, '<\\/script>')}</script>`;
|
|
38
|
+
// Use a function replacer so $& / $' / $` in the bundle are never treated as
|
|
39
|
+
// special replacement patterns by String.replace.
|
|
40
|
+
const html = template.replace('<!--LOWLANDER_DASHBOARD_SCRIPT-->', () => scriptTag);
|
|
41
|
+
|
|
42
|
+
const outFile = resolve(outDir, 'dashboard.html');
|
|
43
|
+
writeFileSync(outFile, html);
|
|
44
|
+
console.log(`Dashboard bundled: ${outFile} (${(html.length / 1024).toFixed(1)} KB)`);
|