lowlander 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +282 -37
  2. package/build/client/client.d.ts +153 -0
  3. package/build/client/client.js +317 -0
  4. package/build/client/client.js.map +1 -0
  5. package/build/examples/helloworld/client/js/admin.d.ts +11 -0
  6. package/build/examples/helloworld/client/js/admin.js +87 -0
  7. package/build/examples/helloworld/client/js/admin.js.map +1 -0
  8. package/build/examples/helloworld/client/js/base.d.ts +4 -0
  9. package/{examples/helloworld/client/js/base.ts → build/examples/helloworld/client/js/base.js} +13 -25
  10. package/build/examples/helloworld/client/js/base.js.map +1 -0
  11. package/build/examples/helloworld/server/api.d.ts +40 -0
  12. package/build/examples/helloworld/server/api.d.ts.map +1 -0
  13. package/{examples/helloworld/server/api.ts → build/examples/helloworld/server/api.js} +58 -66
  14. package/build/examples/helloworld/server/api.js.map +1 -0
  15. package/build/examples/helloworld/server/main.d.ts +2 -0
  16. package/build/examples/helloworld/server/main.d.ts.map +1 -0
  17. package/{examples/helloworld/server/main.ts → build/examples/helloworld/server/main.js} +3 -8
  18. package/build/examples/helloworld/server/main.js.map +1 -0
  19. package/build/server/protocol.d.ts +12 -0
  20. package/build/server/protocol.d.ts.map +1 -0
  21. package/build/server/protocol.js +19 -0
  22. package/build/server/protocol.js.map +1 -0
  23. package/build/server/server.d.ts +191 -0
  24. package/build/server/server.d.ts.map +1 -0
  25. package/build/server/server.js +379 -0
  26. package/build/server/server.js.map +1 -0
  27. package/build/server/wshandler.d.ts +11 -0
  28. package/build/server/wshandler.d.ts.map +1 -0
  29. package/build/server/wshandler.js +126 -0
  30. package/build/server/wshandler.js.map +1 -0
  31. package/build/tsconfig.client.tsbuildinfo +1 -0
  32. package/build/tsconfig.server.tsbuildinfo +1 -0
  33. package/package.json +15 -8
  34. package/server/server.ts +1 -0
  35. package/skill/SKILL.md +605 -0
  36. package/AGENTS.md +0 -2
  37. package/ROADMAP.md +0 -13
  38. package/bun.lock +0 -281
  39. package/examples/helloworld/client/js/admin.ts +0 -94
  40. package/examples/helloworld/package.json +0 -8
  41. package/tests/fake-warpsocket.ts +0 -452
  42. package/tests/helloworld.test.ts +0 -151
  43. package/tsconfig.client.json +0 -18
  44. package/tsconfig.json +0 -24
  45. package/tsconfig.server.json +0 -17
  46. package/tsconfig.test.json +0 -13
  47. /package/{examples → build/examples}/helloworld/client/assets/style.css +0 -0
  48. /package/{examples → build/examples}/helloworld/client/index.html +0 -0
package/README.md CHANGED
@@ -6,8 +6,8 @@ This project is still under heavy development. **DO NOT USE** for anything serio
6
6
 
7
7
  To get an impression of what use of this framework currently looks like, check out the example project's...
8
8
 
9
- - [server-side API](examples/helloworld/server/api.ts) and
10
- - [client-side UI](examples/helloworld/client/js/base.ts) code.
9
+ - [server-side API](https://github.com/vanviegen/lowlander/blob/main/examples/helloworld/server/api.ts) and
10
+ - [client-side UI](https://github.com/vanviegen/lowlander/blob/main/examples/helloworld/client/js/base.ts) code.
11
11
 
12
12
  ## Tech
13
13
 
@@ -20,22 +20,267 @@ This library is built on top of a number of libraries by the same author:
20
20
 
21
21
  Lowlander glues these together and adds real-time partial data synchronization and type-safe RPCs to provide a framework for rapidly building performant full-stack (database included!) web applications.
22
22
 
23
- ## Logging
23
+ ## Tutorial
24
24
 
25
- You can enable debug logging to stdout by setting the `LOWLANDER_LOG_LEVEL` environment variable to a number from 0 to 3. Higher numbers produce more verbose logs, including model-level operations, updates, and reads.
25
+ ### Project Setup
26
+
27
+ ```bash
28
+ bun init
29
+ bun add lowlander aberdeen edinburgh
30
+ ```
31
+
32
+ (npm should also work for all of this.)
33
+
34
+ Create the project structure:
35
+
36
+ ```
37
+ server/
38
+ main.ts # starts the server
39
+ api.ts # exported functions = RPC endpoints
40
+ client/
41
+ app.ts # UI using Aberdeen + Connection
42
+ ```
43
+
44
+ If you use Claude Code, GitHub Copilot or another AI agent that supports Skills, Lowlander and its dependencies include `skill/` directories that provide specialized knowledge to the AI.
45
+
46
+ Symlink them into your project's `.claude/skills` directory:
47
+
48
+ ```bash
49
+ mkdir -p .claude/skills
50
+ ln -s ../../node_modules/lowlander/skill .claude/skills/lowlander
51
+ ln -s ../../node_modules/aberdeen/skill .claude/skills/aberdeen
52
+ ln -s ../../node_modules/edinburgh/skill .claude/skills/edinburgh
53
+ ```
54
+
55
+ ### Server Entry Point
56
+
57
+ The entry point starts the WarpSocket server and points it at the API file:
58
+
59
+ ```ts
60
+ // server/main.ts
61
+ import { start } from 'lowlander/server';
62
+ import { fileURLToPath } from 'url';
63
+ import { resolve, dirname } from 'path';
64
+
65
+ const API_FILE = resolve(dirname(fileURLToPath(import.meta.url)), 'api.js');
66
+ start(API_FILE, { bind: '0.0.0.0:8080' });
67
+ ```
68
+
69
+ Options: `bind` (address:port), `threads` (worker count).
70
+
71
+ ### Defining RPC Endpoints
72
+
73
+ Every exported function in the API file is callable from the client. No decorators or registration needed:
74
+
75
+ ```ts
76
+ // server/api.ts
77
+ export function add(a: number, b: number): number {
78
+ return a + b;
79
+ }
80
+ ```
81
+
82
+ Functions can be `async`. Thrown errors are sent to the client as error responses.
83
+
84
+ ### Edinburgh Models
85
+
86
+ Define persistent data models using Edinburgh. See [Edinburgh docs](https://github.com/vanviegen/edinburgh) for full details.
87
+
88
+ ```ts
89
+ import * as E from 'edinburgh';
90
+
91
+ @E.registerModel
92
+ class Person extends E.Model<Person> {
93
+ static byName = E.primary(Person, 'name');
94
+ name = E.field(E.string);
95
+ age = E.field(E.number);
96
+ friends = E.field(E.array(E.link(Person)));
97
+ password = E.field(E.string);
98
+ }
99
+ ```
100
+
101
+ Models are ACID, and RPC calls automatically run in transactions. When creating a `new Instance()` or updating props on an existing instance, changes are persisted to disk automatically. `E.link` objects are lazy-loaded.
102
+
103
+ ### Model Streaming with `createStreamType`
104
+
105
+ Stream a subset of model fields to clients with real-time updates. Changes are pushed automatically. First you need to create a stream type, by doing this once:
106
+
107
+ ```ts
108
+ import { createStreamType } from 'lowlander/server';
109
+
110
+ // Exclude password; include friends' names and ages
111
+ const PersonStream = createStreamType(Person, {
112
+ name: true,
113
+ age: true,
114
+ friends: { // nested linked model: specify sub-selection
115
+ name: true,
116
+ age: true,
117
+ }
118
+ });
119
+ ```
120
+
121
+ Use `true` for plain fields. For linked model fields, provide a nested selection object. To return a stream instance from an API function:
122
+
123
+ ```ts
124
+ export function streamPerson(name: string) {
125
+ const person = Person.byName.get(name)!;
126
+ return new PersonStream(person);
127
+ }
128
+ ```
129
+
130
+ On the client, this returns a reactive Aberdeen proxy that updates live when server data changes.
131
+
132
+ ### ServerProxy for Stateful APIs
133
+
134
+ Wrap a class instance to expose per-connection stateful methods:
135
+
136
+ ```ts
137
+ import { ServerProxy } from 'lowlander/server';
138
+
139
+ class UserAPI {
140
+ constructor(public userName: string) {}
141
+
142
+ get user(): Person {
143
+ return Person.byName.get(this.userName)!;
144
+ }
145
+
146
+ getBio() {
147
+ return `${this.user.name} is ${this.user.age} years old`;
148
+ }
149
+ }
150
+
151
+ export async function authenticate(token: string) {
152
+ const user = Person.byName.get(token);
153
+ if (!user) throw new Error('User not found');
154
+ return new ServerProxy(new UserAPI(token), 'secret-value');
155
+ }
156
+ ```
157
+
158
+ The client receives `'secret-value'` as `.value` and can call `UserAPI` methods via `.serverProxy`.
159
+
160
+ ### Socket Callbacks
161
+
162
+ Use `Socket<T>` parameters for server-push streaming. On the client, these become callback functions:
163
+
164
+ ```ts
165
+ import { Socket } from 'lowlander/server';
166
+
167
+ export function streamNumbers(socket: Socket<number>) {
168
+ const interval = setInterval(() => {
169
+ if (!socket.send(Math.random())) clearInterval(interval);
170
+ }, 1000);
171
+ }
172
+ ```
173
+
174
+ `socket.send()` returns falsy when the client disconnects.
175
+
176
+ ### Client Connection
177
+
178
+ Connect to the server with full type safety:
179
+
180
+ ```ts
181
+ import { Connection } from 'lowlander/client';
182
+ import type * as API from './server/api.js';
183
+
184
+ const conn = new Connection<typeof API>('ws://localhost:8080/');
185
+ const api = conn.api;
186
+ ```
187
+
188
+ All server exports are available on `conn.api` with matching types, except `Socket<T>` params become callbacks.
189
+
190
+ #### Simple RPC
191
+
192
+ ```ts
193
+ const sum = api.add(1, 2);
194
+ // sum is a PromiseProxy:
195
+ // - sum.value starts out as undefined, and reactively updates to the result when available
196
+ // - sum.error is an Error object if the call threw, or undefined otherwise
197
+ // - sum.promise can be awaited: `const val = await sum.promise;` - this throws on error
198
+ ```
199
+
200
+ #### Using ServerProxy
201
+
202
+ ```ts
203
+ const auth = api.authenticate('Frank');
204
+ // auth.value → 'secret-value' (after resolution)
205
+ // auth.serverProxy → typed proxy to UserAPI methods
206
+
207
+ const bio = auth.serverProxy.getBio();
208
+ // bio.value → "Frank is 45 years old"
209
+ ```
210
+
211
+ The server proxy is usable immediately—calls queue until authentication completes. If auth fails, queued calls fail too.
212
+
213
+ #### Model Streaming
214
+
215
+ ```ts
216
+ const person = api.streamPerson('Alice');
217
+ // person.value is a reactive proxy that auto-updates
218
+ ```
219
+
220
+ #### Socket Callbacks
221
+
222
+ ```ts
223
+ api.streamNumbers(num => console.log(num));
224
+ ```
225
+
226
+ On the server-side we should have a `export function streamNumbers(socket: Socket<number>)`.
227
+
228
+ #### Reactive Integration with Aberdeen
229
+
230
+ `PromiseProxy` results are reactive in Aberdeen scopes:
231
+
232
+ ```ts
233
+ import A from 'aberdeen';
234
+
235
+ const sum = api.add(1, 2);
236
+ A(() => {
237
+ if (sum.busy) A('span#Loading...');
238
+ else if (sum.error) A('span#Error: ' + sum.error.message);
239
+ else A('span#Result: ' + sum.value);
240
+ });
241
+ ```
242
+
243
+ Model streams are also reactive—nested data updates trigger fine-grained UI updates:
244
+
245
+ ```ts
246
+ const model = api.streamModel();
247
+ A(() => {
248
+ if (!model.value) return;
249
+ A('h2#' + model.value.name);
250
+ A('p#Owner: ' + model.value.owner.name);
251
+ });
252
+ ```
253
+
254
+ #### Connection Status
255
+
256
+ ```ts
257
+ A(() => {
258
+ A('span#' + (conn.isOnline() ? 'Connected' : 'Offline'));
259
+ });
260
+ ```
261
+
262
+ Reconnection is automatic with exponential backoff.
263
+
264
+ #### Cleanup
265
+
266
+ Aberdeen's `clean()` handles RPC lifecycle. When a reactive scope is destroyed, active requests and subscriptions are cancelled automatically.
267
+
268
+ ### Logging
269
+
270
+ Set the `LOWLANDER_LOG_LEVEL` environment variable to a number from 0 to 3:
26
271
 
27
272
  - 0: no logging (default)
28
273
  - 1: connections & lifecycle
29
274
  - 2: RPC calls & responses
30
275
  - 3: model streaming & internals
31
276
 
32
- You can set a similar `EDINBURGH_LOG_LEVEL` variable to enable logging from the underlying Edinburgh library, which Lowlander uses for data management and synchronization.
277
+ Set `EDINBURGH_LOG_LEVEL` similarly for Edinburgh internals.
33
278
 
34
279
  ## Server API Reference
35
280
 
36
281
  The following is auto-generated from `server/server.ts`:
37
282
 
38
- ### createStreamType · [function](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L162)
283
+ ### createStreamType · [function](https://github.com/vanviegen/lowlander/blob/main/server/server.ts#L162)
39
284
 
40
285
  Creates a stream type for reactive model streaming to clients with automatic updates.
41
286
 
@@ -60,7 +305,7 @@ Supports nested linked models and type-safe field selection.
60
305
 
61
306
  ```ts
62
307
 
63
- ### sendModel · [function](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L262)
308
+ ### sendModel · [function](https://github.com/vanviegen/lowlander/blob/main/server/server.ts#L262)
64
309
 
65
310
  Sends (updated) data for `model` to `target`.
66
311
  `target` is a virtual socket with a requestId+'d' user prefix, or a channel that subscribes such virtual sockets.
@@ -75,7 +320,7 @@ Sends (updated) data for `model` to `target`.
75
320
  - `StreamType: typeof StreamTypeBase<any>`
76
321
  - `changed?: E.Change`
77
322
 
78
- ### pushModel · [function](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L318)
323
+ ### pushModel · [function](https://github.com/vanviegen/lowlander/blob/main/server/server.ts#L318)
79
324
 
80
325
  Subscribes `target` to this model, and sends initial data.
81
326
  `target` is a virtual socket with a requestId+'d' user prefix, or a channel that subscribes such virtual sockets.
@@ -90,11 +335,11 @@ Subscribes `target` to this model, and sends initial data.
90
335
  - `SubStreamType: typeof StreamTypeBase<any>`
91
336
  - `delta: number`
92
337
 
93
- ### start · [function](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L427)
338
+ ### start · [function](https://github.com/vanviegen/lowlander/blob/main/server/server.ts#L428)
94
339
 
95
340
  Starts the Lowlander WebSocket server.
96
341
 
97
- **Signature:** `(mainApiFile: string, opts?: { bind?: string; threads?: number; injectWarpSocket?: typeof import("/var/home/frank/projects/warpsocket/dist/src/index", { with: { "resolution-mode": "import" } }); }) => Promise<void>`
342
+ **Signature:** `(mainApiFile: string, opts?: { bind?: string; threads?: number; injectWarpSocket?: typeof import("/var/home/frank/projects/lowlander/node_modules/warpsocket/dist/src/index", { with: { "resolution-mode": "import" } }); }) => Promise<void>`
98
343
 
99
344
  **Parameters:**
100
345
 
@@ -112,15 +357,15 @@ const API_FILE = resolve(dirname(fileURLToPath(import.meta.url)), 'api.js');
112
357
  start(API_FILE, { bind: '0.0.0.0:8080' });
113
358
  ```
114
359
 
115
- ### logLevel · [constant](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L10)
360
+ ### logLevel · [constant](https://github.com/vanviegen/lowlander/blob/main/server/server.ts#L10)
116
361
 
117
362
  **Value:** `number`
118
363
 
119
- ### warpsocket · [class](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L13)
364
+ ### warpsocket · [class](https://github.com/vanviegen/lowlander/blob/main/server/server.ts#L13)
120
365
 
121
- **Type:** `typeof import("/var/home/frank/projects/warpsocket/dist/src/index", { with: { "resolution-mode": "import" } })`
366
+ **Type:** `typeof import("/var/home/frank/projects/lowlander/node_modules/warpsocket/dist/src/index", { with: { "resolution-mode": "import" } })`
122
367
 
123
- ### StreamTypeBase · [abstract class](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L34)
368
+ ### StreamTypeBase · [abstract class](https://github.com/vanviegen/lowlander/blob/main/server/server.ts#L34)
124
369
 
125
370
  [object Object],[object Object],[object Object]
126
371
 
@@ -128,22 +373,22 @@ start(API_FILE, { bind: '0.0.0.0:8080' });
128
373
 
129
374
  - `T`
130
375
 
131
- #### StreamTypeBase.fields · [static property](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L36)
376
+ #### StreamTypeBase.fields · [static property](https://github.com/vanviegen/lowlander/blob/main/server/server.ts#L36)
132
377
 
133
378
  **Type:** `{ [key: string]: number | true; }`
134
379
 
135
- #### StreamTypeBase.id · [static property](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L38)
380
+ #### StreamTypeBase.id · [static property](https://github.com/vanviegen/lowlander/blob/main/server/server.ts#L38)
136
381
 
137
382
  **Type:** `number`
138
383
 
139
- #### streamTypeBase.toString · [method](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L42)
384
+ #### streamTypeBase.toString · [method](https://github.com/vanviegen/lowlander/blob/main/server/server.ts#L42)
140
385
 
141
386
  **Signature:** `() => string`
142
387
 
143
388
  **Parameters:**
144
389
 
145
390
 
146
- ### ServerProxy · [class](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L349)
391
+ ### ServerProxy · [class](https://github.com/vanviegen/lowlander/blob/main/server/server.ts#L349)
147
392
 
148
393
  Wraps a server-side API object to create a stateful, type-safe proxy accessible from clients.
149
394
  Use for authentication, sessions, or any stateful context that persists across RPC calls.
@@ -174,14 +419,14 @@ export async function authenticate(token: string) {
174
419
  - `api`: - Server-side API object exposed to the client
175
420
  - `value`: - Value returned immediately to the client
176
421
 
177
- #### serverProxy.toString · [method](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L355)
422
+ #### serverProxy.toString · [method](https://github.com/vanviegen/lowlander/blob/main/server/server.ts#L355)
178
423
 
179
424
  **Signature:** `() => string`
180
425
 
181
426
  **Parameters:**
182
427
 
183
428
 
184
- ### Socket · [class](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L379)
429
+ ### Socket · [class](https://github.com/vanviegen/lowlander/blob/main/server/server.ts#L379)
185
430
 
186
431
  Server-side socket for pushing data to a client. Server functions with `Socket<T>` parameters
187
432
  receive client callbacks on the client side.
@@ -204,7 +449,7 @@ export function streamNumbers(socket: Socket<number>) {
204
449
  api.streamNumbers(num => console.log(num));
205
450
  ```
206
451
 
207
- #### socket.send · [method](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L388)
452
+ #### socket.send · [method](https://github.com/vanviegen/lowlander/blob/main/server/server.ts#L388)
208
453
 
209
454
  Sends data to the client.
210
455
 
@@ -216,7 +461,7 @@ Sends data to the client.
216
461
 
217
462
  **Returns:** `true` if sent, `false` if socket is closed
218
463
 
219
- #### socket.subscribe · [method](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L394)
464
+ #### socket.subscribe · [method](https://github.com/vanviegen/lowlander/blob/main/server/server.ts#L394)
220
465
 
221
466
  **Signature:** `(channel: Uint8Array<ArrayBufferLike>, delta?: number) => void`
222
467
 
@@ -225,14 +470,14 @@ Sends data to the client.
225
470
  - `channel: Uint8Array`
226
471
  - `delta: any` (optional)
227
472
 
228
- #### socket.toString · [method](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L401)
473
+ #### socket.toString · [method](https://github.com/vanviegen/lowlander/blob/main/server/server.ts#L401)
229
474
 
230
475
  **Signature:** `() => string`
231
476
 
232
477
  **Parameters:**
233
478
 
234
479
 
235
- #### socket.[Symbol.for('nodejs.util.inspect.custom')] · [method](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L405)
480
+ #### socket.[Symbol.for('nodejs.util.inspect.custom')] · [method](https://github.com/vanviegen/lowlander/blob/main/server/server.ts#L405)
236
481
 
237
482
  **Signature:** `() => string`
238
483
 
@@ -243,13 +488,13 @@ Sends data to the client.
243
488
 
244
489
  The following is auto-generated from `client/client.ts`:
245
490
 
246
- ### logLevel · [variable](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L8)
491
+ ### logLevel · [variable](https://github.com/vanviegen/lowlander/blob/main/client/client.ts#L8)
247
492
 
248
493
  Set to 1-3 for increasing verbosity.
249
494
 
250
495
  **Value:** `number`
251
496
 
252
- ### ClientProxyObject · [type](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L157)
497
+ ### ClientProxyObject · [type](https://github.com/vanviegen/lowlander/blob/main/client/client.ts#L157)
253
498
 
254
499
  Transforms server-side API objects to client-side proxy objects with type-safe RPC methods.
255
500
 
@@ -257,7 +502,7 @@ Transforms server-side API objects to client-side proxy objects with type-safe R
257
502
  [K in keyof T]: ClientProxyFunction<T[K]>
258
503
  }`
259
504
 
260
- ### Connection · [class](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L190)
505
+ ### Connection · [class](https://github.com/vanviegen/lowlander/blob/main/client/client.ts#L190)
261
506
 
262
507
  WebSocket connection to a Lowlander server with type-safe RPC, automatic reconnection,
263
508
  and reactive updates.
@@ -293,27 +538,27 @@ $(() => {
293
538
 
294
539
  - `url`: - WebSocket URL (e.g., 'ws://localhost:8080/'), or a fake WebSocket object for testing
295
540
 
296
- #### connection.ws · [property](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L192)
541
+ #### connection.ws · [property](https://github.com/vanviegen/lowlander/blob/main/client/client.ts#L192)
297
542
 
298
543
  **Type:** `WebSocket`
299
544
 
300
- #### connection.activeRequests · [property](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L193)
545
+ #### connection.activeRequests · [property](https://github.com/vanviegen/lowlander/blob/main/client/client.ts#L193)
301
546
 
302
547
  **Type:** `Map<number, ActiveRequest>`
303
548
 
304
- #### connection.requestCounter · [property](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L194)
549
+ #### connection.requestCounter · [property](https://github.com/vanviegen/lowlander/blob/main/client/client.ts#L194)
305
550
 
306
551
  **Type:** `number`
307
552
 
308
- #### connection.reconnectAttempts · [property](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L195)
553
+ #### connection.reconnectAttempts · [property](https://github.com/vanviegen/lowlander/blob/main/client/client.ts#L195)
309
554
 
310
555
  **Type:** `number`
311
556
 
312
- #### connection.onlineProxy · [property](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L198)
557
+ #### connection.onlineProxy · [property](https://github.com/vanviegen/lowlander/blob/main/client/client.ts#L198)
313
558
 
314
559
  **Type:** `ValueRef<boolean>`
315
560
 
316
- #### connection.api · [property](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L205)
561
+ #### connection.api · [property](https://github.com/vanviegen/lowlander/blob/main/client/client.ts#L205)
317
562
 
318
563
  Type-safe proxy to the server-side API. Methods return `PromiseProxy` objects
319
564
  that work reactively in Aberdeen scopes. `ServerProxy` returns include a
@@ -321,7 +566,7 @@ that work reactively in Aberdeen scopes. `ServerProxy` returns include a
321
566
 
322
567
  **Type:** `ClientProxyObject<T>`
323
568
 
324
- #### connection.isOnline · [method](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L218)
569
+ #### connection.isOnline · [method](https://github.com/vanviegen/lowlander/blob/main/client/client.ts#L218)
325
570
 
326
571
  Returns the current connection status. Reactive in Aberdeen scopes.
327
572
 
@@ -330,21 +575,21 @@ Returns the current connection status. Reactive in Aberdeen scopes.
330
575
  **Parameters:**
331
576
 
332
577
 
333
- #### connection.connect · [method](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L220)
578
+ #### connection.connect · [method](https://github.com/vanviegen/lowlander/blob/main/client/client.ts#L220)
334
579
 
335
580
  **Signature:** `() => void`
336
581
 
337
582
  **Parameters:**
338
583
 
339
584
 
340
- #### connection.reconnect · [method](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L374)
585
+ #### connection.reconnect · [method](https://github.com/vanviegen/lowlander/blob/main/client/client.ts#L374)
341
586
 
342
587
  **Signature:** `() => void`
343
588
 
344
589
  **Parameters:**
345
590
 
346
591
 
347
- #### connection.pruneCommitIds · [method](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L388)
592
+ #### connection.pruneCommitIds · [method](https://github.com/vanviegen/lowlander/blob/main/client/client.ts#L388)
348
593
 
349
594
  **Signature:** `(request: ActiveRequest, maxCommitId: number) => void`
350
595
 
@@ -0,0 +1,153 @@
1
+ import type { Socket, ServerProxy, StreamTypeBase } from '../server/server.js';
2
+ import type { PromiseProxy } from 'aberdeen';
3
+ /** Set to 1-3 for increasing verbosity. */
4
+ export declare let logLevel: number;
5
+ /**
6
+ * Transforms server-side `Socket<T>` arguments to client-side callback functions `(data: T) => void`.
7
+ *
8
+ * @typeParam A - The server-side argument type
9
+ */
10
+ type ClientProxyArg<A> = A extends Socket<infer U> ? (data: U) => void : A;
11
+ /**
12
+ * Recursively transforms all server-side function arguments for client-side use.
13
+ *
14
+ * @typeParam Args - Tuple of server-side argument types
15
+ */
16
+ type ClientProxyArgs<Args extends any[]> = Args extends [infer A, ...infer Rest] ? [ClientProxyArg<A>, ...ClientProxyArgs<Rest>] : [];
17
+ /**
18
+ * Transforms server-side return types for client-side use.
19
+ *
20
+ * - Strips `Promise` wrappers
21
+ * - `ServerProxy<API, RETURN>` → `PromiseProxy<RETURN> & {serverProxy: ClientProxyObject<API>}`
22
+ * - Other types → `PromiseProxy<R>`
23
+ *
24
+ * @typeParam R - The server-side return type
25
+ */
26
+ type ClientProxyReturn<R> = R extends Promise<infer U> ? ClientProxyReturn<U> : R extends ServerProxy<infer API, infer RETURN> ? PromiseProxy<RETURN> & {
27
+ promise: Promise<RETURN>;
28
+ serverProxy: ClientProxyObject<API>;
29
+ } : R extends StreamTypeBase<infer T> ? PromiseProxy<T> & {
30
+ promise: Promise<T>;
31
+ } : PromiseProxy<R> & {
32
+ promise: Promise<R>;
33
+ };
34
+ /**
35
+ * Transforms server-side function signatures for client-side proxy use.
36
+ * This type correctly handles function overloads by explicitly matching
37
+ * up to 8 signatures and creating an intersection of the transformed types.
38
+ *
39
+ * @typeParam T - The server-side function type, which may be overloaded.
40
+ */
41
+ type ClientProxyFunction<T> = T extends {
42
+ (...args: infer A1): infer R1;
43
+ (...args: infer A2): infer R2;
44
+ (...args: infer A3): infer R3;
45
+ (...args: infer A4): infer R4;
46
+ (...args: infer A5): infer R5;
47
+ (...args: infer A6): infer R6;
48
+ (...args: infer A7): infer R7;
49
+ (...args: infer A8): infer R8;
50
+ } ? ((...args: ClientProxyArgs<A1>) => ClientProxyReturn<R1>) & ((...args: ClientProxyArgs<A2>) => ClientProxyReturn<R2>) & ((...args: ClientProxyArgs<A3>) => ClientProxyReturn<R3>) & ((...args: ClientProxyArgs<A4>) => ClientProxyReturn<R4>) & ((...args: ClientProxyArgs<A5>) => ClientProxyReturn<R5>) & ((...args: ClientProxyArgs<A6>) => ClientProxyReturn<R6>) & ((...args: ClientProxyArgs<A7>) => ClientProxyReturn<R7>) & ((...args: ClientProxyArgs<A8>) => ClientProxyReturn<R8>) : T extends {
51
+ (...args: infer A1): infer R1;
52
+ (...args: infer A2): infer R2;
53
+ (...args: infer A3): infer R3;
54
+ (...args: infer A4): infer R4;
55
+ (...args: infer A5): infer R5;
56
+ (...args: infer A6): infer R6;
57
+ (...args: infer A7): infer R7;
58
+ } ? ((...args: ClientProxyArgs<A1>) => ClientProxyReturn<R1>) & ((...args: ClientProxyArgs<A2>) => ClientProxyReturn<R2>) & ((...args: ClientProxyArgs<A3>) => ClientProxyReturn<R3>) & ((...args: ClientProxyArgs<A4>) => ClientProxyReturn<R4>) & ((...args: ClientProxyArgs<A5>) => ClientProxyReturn<R5>) & ((...args: ClientProxyArgs<A6>) => ClientProxyReturn<R6>) & ((...args: ClientProxyArgs<A7>) => ClientProxyReturn<R7>) : T extends {
59
+ (...args: infer A1): infer R1;
60
+ (...args: infer A2): infer R2;
61
+ (...args: infer A3): infer R3;
62
+ (...args: infer A4): infer R4;
63
+ (...args: infer A5): infer R5;
64
+ (...args: infer A6): infer R6;
65
+ } ? ((...args: ClientProxyArgs<A1>) => ClientProxyReturn<R1>) & ((...args: ClientProxyArgs<A2>) => ClientProxyReturn<R2>) & ((...args: ClientProxyArgs<A3>) => ClientProxyReturn<R3>) & ((...args: ClientProxyArgs<A4>) => ClientProxyReturn<R4>) & ((...args: ClientProxyArgs<A5>) => ClientProxyReturn<R5>) & ((...args: ClientProxyArgs<A6>) => ClientProxyReturn<R6>) : T extends {
66
+ (...args: infer A1): infer R1;
67
+ (...args: infer A2): infer R2;
68
+ (...args: infer A3): infer R3;
69
+ (...args: infer A4): infer R4;
70
+ (...args: infer A5): infer R5;
71
+ } ? ((...args: ClientProxyArgs<A1>) => ClientProxyReturn<R1>) & ((...args: ClientProxyArgs<A2>) => ClientProxyReturn<R2>) & ((...args: ClientProxyArgs<A3>) => ClientProxyReturn<R3>) & ((...args: ClientProxyArgs<A4>) => ClientProxyReturn<R4>) & ((...args: ClientProxyArgs<A5>) => ClientProxyReturn<R5>) : T extends {
72
+ (...args: infer A1): infer R1;
73
+ (...args: infer A2): infer R2;
74
+ (...args: infer A3): infer R3;
75
+ (...args: infer A4): infer R4;
76
+ } ? ((...args: ClientProxyArgs<A1>) => ClientProxyReturn<R1>) & ((...args: ClientProxyArgs<A2>) => ClientProxyReturn<R2>) & ((...args: ClientProxyArgs<A3>) => ClientProxyReturn<R3>) & ((...args: ClientProxyArgs<A4>) => ClientProxyReturn<R4>) : T extends {
77
+ (...args: infer A1): infer R1;
78
+ (...args: infer A2): infer R2;
79
+ (...args: infer A3): infer R3;
80
+ } ? ((...args: ClientProxyArgs<A1>) => ClientProxyReturn<R1>) & ((...args: ClientProxyArgs<A2>) => ClientProxyReturn<R2>) & ((...args: ClientProxyArgs<A3>) => ClientProxyReturn<R3>) : T extends {
81
+ (...args: infer A1): infer R1;
82
+ (...args: infer A2): infer R2;
83
+ } ? ((...args: ClientProxyArgs<A1>) => ClientProxyReturn<R1>) & ((...args: ClientProxyArgs<A2>) => ClientProxyReturn<R2>) : T extends (...args: infer A) => infer R ? (...args: ClientProxyArgs<A>) => ClientProxyReturn<R> : never;
84
+ /**
85
+ * Transforms server-side API objects to client-side proxy objects with type-safe RPC methods.
86
+ *
87
+ * @typeParam T - The server-side API object type
88
+ */
89
+ export type ClientProxyObject<T> = {
90
+ [K in keyof T]: ClientProxyFunction<T[K]>;
91
+ };
92
+ /**
93
+ * WebSocket connection to a Lowlander server with type-safe RPC, automatic reconnection,
94
+ * and reactive updates.
95
+ *
96
+ * @typeParam T - The server-side API type (import from your server API file)
97
+ *
98
+ * @example
99
+ * ```ts
100
+ * import type * as API from './server/api.js';
101
+ * const conn = new Connection<typeof API>('ws://localhost:8080/');
102
+ *
103
+ * // Simple RPC - returns PromiseProxy
104
+ * const sum = conn.api.add(1, 2);
105
+ *
106
+ * // Server proxy for stateful APIs
107
+ * const auth = conn.api.authenticate('token');
108
+ * const secret = auth.serverProxy.getSecret();
109
+ *
110
+ * // Streaming with callbacks
111
+ * conn.api.streamData(data => console.log(data));
112
+ *
113
+ * // Use within Aberdeen reactive scopes
114
+ * $(() => {
115
+ * dump(conn.isOnline());
116
+ * dump(sum);
117
+ * });
118
+ * ```
119
+ */
120
+ export declare class Connection<T> {
121
+ url: string | (() => WebSocket);
122
+ private ws?;
123
+ private activeRequests;
124
+ private requestCounter;
125
+ private reconnectAttempts;
126
+ /** @internal */
127
+ _proxyCounter: number;
128
+ private onlineProxy;
129
+ /**
130
+ * Type-safe proxy to the server-side API. Methods return `PromiseProxy` objects
131
+ * that work reactively in Aberdeen scopes. `ServerProxy` returns include a
132
+ * `.serverProxy` property for accessing stateful server APIs.
133
+ */
134
+ api: ClientProxyObject<T>;
135
+ /**
136
+ * @param url - WebSocket URL (e.g., 'ws://localhost:8080/'), or a fake WebSocket object for testing
137
+ */
138
+ constructor(url: string | (() => WebSocket));
139
+ /**
140
+ * Returns the current connection status. Reactive in Aberdeen scopes.
141
+ */
142
+ isOnline(): boolean;
143
+ private connect;
144
+ private reconnect;
145
+ private pruneCommitIds;
146
+ /** @internal */
147
+ _createMethodStub(methodName: string, proxyId?: number): (...params: any[]) => PromiseProxy<any> & {
148
+ promise: Promise<any>;
149
+ } & {
150
+ serverProxy: any;
151
+ };
152
+ }
153
+ export {};