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.
- package/README.md +282 -37
- package/build/client/client.d.ts +153 -0
- package/build/client/client.js +317 -0
- package/build/client/client.js.map +1 -0
- package/build/examples/helloworld/client/js/admin.d.ts +11 -0
- package/build/examples/helloworld/client/js/admin.js +87 -0
- package/build/examples/helloworld/client/js/admin.js.map +1 -0
- package/build/examples/helloworld/client/js/base.d.ts +4 -0
- package/{examples/helloworld/client/js/base.ts → build/examples/helloworld/client/js/base.js} +13 -25
- package/build/examples/helloworld/client/js/base.js.map +1 -0
- package/build/examples/helloworld/server/api.d.ts +40 -0
- package/build/examples/helloworld/server/api.d.ts.map +1 -0
- package/{examples/helloworld/server/api.ts → build/examples/helloworld/server/api.js} +58 -66
- package/build/examples/helloworld/server/api.js.map +1 -0
- package/build/examples/helloworld/server/main.d.ts +2 -0
- package/build/examples/helloworld/server/main.d.ts.map +1 -0
- package/{examples/helloworld/server/main.ts → build/examples/helloworld/server/main.js} +3 -8
- package/build/examples/helloworld/server/main.js.map +1 -0
- package/build/server/protocol.d.ts +12 -0
- package/build/server/protocol.d.ts.map +1 -0
- package/build/server/protocol.js +19 -0
- package/build/server/protocol.js.map +1 -0
- package/build/server/server.d.ts +191 -0
- package/build/server/server.d.ts.map +1 -0
- package/build/server/server.js +379 -0
- package/build/server/server.js.map +1 -0
- package/build/server/wshandler.d.ts +11 -0
- package/build/server/wshandler.d.ts.map +1 -0
- package/build/server/wshandler.js +126 -0
- package/build/server/wshandler.js.map +1 -0
- package/build/tsconfig.client.tsbuildinfo +1 -0
- package/build/tsconfig.server.tsbuildinfo +1 -0
- package/package.json +15 -8
- package/server/server.ts +1 -0
- package/skill/SKILL.md +605 -0
- package/AGENTS.md +0 -2
- package/ROADMAP.md +0 -13
- package/bun.lock +0 -281
- package/examples/helloworld/client/js/admin.ts +0 -94
- package/examples/helloworld/package.json +0 -8
- package/tests/fake-warpsocket.ts +0 -452
- package/tests/helloworld.test.ts +0 -151
- package/tsconfig.client.json +0 -18
- package/tsconfig.json +0 -24
- package/tsconfig.server.json +0 -17
- package/tsconfig.test.json +0 -13
- /package/{examples → build/examples}/helloworld/client/assets/style.css +0 -0
- /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
|
-
##
|
|
23
|
+
## Tutorial
|
|
24
24
|
|
|
25
|
-
|
|
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
|
-
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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 {};
|