lowlander 0.2.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/AGENTS.md +2 -0
- package/LICENSE +15 -0
- package/README.md +355 -0
- package/ROADMAP.md +13 -0
- package/bun.lock +281 -0
- package/client/client.ts +495 -0
- package/examples/helloworld/client/assets/style.css +45 -0
- package/examples/helloworld/client/index.html +22 -0
- package/examples/helloworld/client/js/admin.ts +94 -0
- package/examples/helloworld/client/js/base.ts +83 -0
- package/examples/helloworld/package.json +8 -0
- package/examples/helloworld/server/api.ts +154 -0
- package/examples/helloworld/server/main.ts +27 -0
- package/package.json +50 -0
- package/server/protocol.ts +22 -0
- package/server/server.ts +437 -0
- package/server/wshandler.ts +138 -0
- package/tests/fake-warpsocket.ts +452 -0
- package/tests/helloworld.test.ts +151 -0
- package/tsconfig.client.json +18 -0
- package/tsconfig.json +24 -0
- package/tsconfig.server.json +17 -0
- package/tsconfig.test.json +13 -0
package/AGENTS.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Frank van Viegen
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
10
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
11
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
12
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
13
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
14
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
15
|
+
PERFORMANCE OF THIS SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
# Lowlander
|
|
2
|
+
|
|
3
|
+
An **experimental** TypeScript framework for data persistence and (partial) client synchronization.
|
|
4
|
+
|
|
5
|
+
This project is still under heavy development. **DO NOT USE** for anything serious. Early feedback is very welcome though!
|
|
6
|
+
|
|
7
|
+
To get an impression of what use of this framework currently looks like, check out the example project's...
|
|
8
|
+
|
|
9
|
+
- [server-side API](examples/helloworld/server/api.ts) and
|
|
10
|
+
- [client-side UI](examples/helloworld/client/js/base.ts) code.
|
|
11
|
+
|
|
12
|
+
## Tech
|
|
13
|
+
|
|
14
|
+
This library is built on top of a number of libraries by the same author:
|
|
15
|
+
|
|
16
|
+
- [Edinburgh](https://github.com/vanviegen/edinburgh): use JavaScript objects as really fast ACID database records.
|
|
17
|
+
- [OLMDB](https://github.com/vanviegen/olmdb): a very fast on-disk key/value store with MVCC and optimistic transactions, used by Edinburgh for persistence.
|
|
18
|
+
- [WarpSocket](https://github.com/vanviegen/warpsocket): a high-performance WebSocket server written in Rust, that coordinates multiple JavaScript worker threads and provides an API for channel subscriptions.
|
|
19
|
+
- [Aberdeen](https://github.com/vanviegen/aberdeen): a reactive UI library for JavaScript. It features fine-grained updates, needs no virtual DOM, and uses Proxy for reactivity.
|
|
20
|
+
|
|
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
|
+
|
|
23
|
+
## Logging
|
|
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.
|
|
26
|
+
|
|
27
|
+
- 0: no logging (default)
|
|
28
|
+
- 1: connections & lifecycle
|
|
29
|
+
- 2: RPC calls & responses
|
|
30
|
+
- 3: model streaming & internals
|
|
31
|
+
|
|
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.
|
|
33
|
+
|
|
34
|
+
## Server API Reference
|
|
35
|
+
|
|
36
|
+
The following is auto-generated from `server/server.ts`:
|
|
37
|
+
|
|
38
|
+
### createStreamType · [function](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L162)
|
|
39
|
+
|
|
40
|
+
Creates a stream type for reactive model streaming to clients with automatic updates.
|
|
41
|
+
|
|
42
|
+
Specify which fields to include; when they change, updates are pushed to subscribed clients.
|
|
43
|
+
Supports nested linked models and type-safe field selection.
|
|
44
|
+
|
|
45
|
+
**Signature:** `<T, S extends FieldSelection<T>>(Model: typeof E.Model<unknown> & (new (...args: any[]) => T), selection: S & ValidateSelection<T, S>) => typeof StreamType`
|
|
46
|
+
|
|
47
|
+
**Type Parameters:**
|
|
48
|
+
|
|
49
|
+
- `T`
|
|
50
|
+
- `S extends FieldSelection<T>`
|
|
51
|
+
|
|
52
|
+
**Parameters:**
|
|
53
|
+
|
|
54
|
+
- `Model: ModelClass & (new (...args: any[]) => T)` - - The Edinburgh model class
|
|
55
|
+
- `selection: S & ValidateSelection<T, S>` - - Field selection: `true` for simple fields, nested object for linked models
|
|
56
|
+
|
|
57
|
+
**Returns:** Stream type class to instantiate in API functions
|
|
58
|
+
|
|
59
|
+
**Examples:**
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
|
|
63
|
+
### sendModel · [function](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L262)
|
|
64
|
+
|
|
65
|
+
Sends (updated) data for `model` to `target`.
|
|
66
|
+
`target` is a virtual socket with a requestId+'d' user prefix, or a channel that subscribes such virtual sockets.
|
|
67
|
+
|
|
68
|
+
**Signature:** `(target: number | Uint8Array<ArrayBufferLike> | number[], model: Model<any>, commitId: number, StreamType: typeof StreamTypeBase<any>, changed?: Change) => void`
|
|
69
|
+
|
|
70
|
+
**Parameters:**
|
|
71
|
+
|
|
72
|
+
- `target: Uint8Array | number | number[]`
|
|
73
|
+
- `model: E.Model<any>`
|
|
74
|
+
- `commitId: number`
|
|
75
|
+
- `StreamType: typeof StreamTypeBase<any>`
|
|
76
|
+
- `changed?: E.Change`
|
|
77
|
+
|
|
78
|
+
### pushModel · [function](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L318)
|
|
79
|
+
|
|
80
|
+
Subscribes `target` to this model, and sends initial data.
|
|
81
|
+
`target` is a virtual socket with a requestId+'d' user prefix, or a channel that subscribes such virtual sockets.
|
|
82
|
+
|
|
83
|
+
**Signature:** `(target: number | Uint8Array<ArrayBufferLike> | number[], model: Model<any>, commitId: number, SubStreamType: typeof StreamTypeBase<any>, delta: number) => void`
|
|
84
|
+
|
|
85
|
+
**Parameters:**
|
|
86
|
+
|
|
87
|
+
- `target: number | Uint8Array | number[]`
|
|
88
|
+
- `model: E.Model<any>`
|
|
89
|
+
- `commitId: number`
|
|
90
|
+
- `SubStreamType: typeof StreamTypeBase<any>`
|
|
91
|
+
- `delta: number`
|
|
92
|
+
|
|
93
|
+
### start · [function](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L427)
|
|
94
|
+
|
|
95
|
+
Starts the Lowlander WebSocket server.
|
|
96
|
+
|
|
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>`
|
|
98
|
+
|
|
99
|
+
**Parameters:**
|
|
100
|
+
|
|
101
|
+
- `mainApiFile: string` - - Absolute path to the compiled API file exporting server functions
|
|
102
|
+
- `opts: {bind?: string, threads?: number, injectWarpSocket?: typeof realWarpsocket}` (optional)
|
|
103
|
+
|
|
104
|
+
**Examples:**
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { start } from 'lowlander/server';
|
|
108
|
+
import { fileURLToPath } from 'url';
|
|
109
|
+
import { resolve, dirname } from 'path';
|
|
110
|
+
|
|
111
|
+
const API_FILE = resolve(dirname(fileURLToPath(import.meta.url)), 'api.js');
|
|
112
|
+
start(API_FILE, { bind: '0.0.0.0:8080' });
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### logLevel · [constant](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L10)
|
|
116
|
+
|
|
117
|
+
**Value:** `number`
|
|
118
|
+
|
|
119
|
+
### warpsocket · [class](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L13)
|
|
120
|
+
|
|
121
|
+
**Type:** `typeof import("/var/home/frank/projects/warpsocket/dist/src/index", { with: { "resolution-mode": "import" } })`
|
|
122
|
+
|
|
123
|
+
### StreamTypeBase · [abstract class](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L34)
|
|
124
|
+
|
|
125
|
+
[object Object],[object Object],[object Object]
|
|
126
|
+
|
|
127
|
+
**Type Parameters:**
|
|
128
|
+
|
|
129
|
+
- `T`
|
|
130
|
+
|
|
131
|
+
#### StreamTypeBase.fields · [static property](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L36)
|
|
132
|
+
|
|
133
|
+
**Type:** `{ [key: string]: number | true; }`
|
|
134
|
+
|
|
135
|
+
#### StreamTypeBase.id · [static property](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L38)
|
|
136
|
+
|
|
137
|
+
**Type:** `number`
|
|
138
|
+
|
|
139
|
+
#### streamTypeBase.toString · [method](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L42)
|
|
140
|
+
|
|
141
|
+
**Signature:** `() => string`
|
|
142
|
+
|
|
143
|
+
**Parameters:**
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
### ServerProxy · [class](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L349)
|
|
147
|
+
|
|
148
|
+
Wraps a server-side API object to create a stateful, type-safe proxy accessible from clients.
|
|
149
|
+
Use for authentication, sessions, or any stateful context that persists across RPC calls.
|
|
150
|
+
|
|
151
|
+
**Type Parameters:**
|
|
152
|
+
|
|
153
|
+
- `API extends object`
|
|
154
|
+
- `RETURN`
|
|
155
|
+
|
|
156
|
+
**Examples:**
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
export class UserAPI {
|
|
160
|
+
constructor(public user: User) {}
|
|
161
|
+
getSecret() { return this.user.secret; }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export async function authenticate(token: string) {
|
|
165
|
+
const user = await validateToken(token);
|
|
166
|
+
return new ServerProxy(new UserAPI(user), user.name);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Client: auth.value is user name, auth.serverProxy.getSecret() calls UserAPI method
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Constructor Parameters:**
|
|
173
|
+
|
|
174
|
+
- `api`: - Server-side API object exposed to the client
|
|
175
|
+
- `value`: - Value returned immediately to the client
|
|
176
|
+
|
|
177
|
+
#### serverProxy.toString · [method](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L355)
|
|
178
|
+
|
|
179
|
+
**Signature:** `() => string`
|
|
180
|
+
|
|
181
|
+
**Parameters:**
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
### Socket · [class](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L379)
|
|
185
|
+
|
|
186
|
+
Server-side socket for pushing data to a client. Server functions with `Socket<T>` parameters
|
|
187
|
+
receive client callbacks on the client side.
|
|
188
|
+
|
|
189
|
+
**Type Parameters:**
|
|
190
|
+
|
|
191
|
+
- `T`
|
|
192
|
+
|
|
193
|
+
**Examples:**
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
// Server
|
|
197
|
+
export function streamNumbers(socket: Socket<number>) {
|
|
198
|
+
setInterval(() => {
|
|
199
|
+
if (!socket.send(Math.random())) clearInterval(interval);
|
|
200
|
+
}, 1000);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Client
|
|
204
|
+
api.streamNumbers(num => console.log(num));
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
#### socket.send · [method](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L388)
|
|
208
|
+
|
|
209
|
+
Sends data to the client.
|
|
210
|
+
|
|
211
|
+
**Signature:** `(data: T) => number`
|
|
212
|
+
|
|
213
|
+
**Parameters:**
|
|
214
|
+
|
|
215
|
+
- `data: T` - - Data to send (automatically serialized)
|
|
216
|
+
|
|
217
|
+
**Returns:** `true` if sent, `false` if socket is closed
|
|
218
|
+
|
|
219
|
+
#### socket.subscribe · [method](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L394)
|
|
220
|
+
|
|
221
|
+
**Signature:** `(channel: Uint8Array<ArrayBufferLike>, delta?: number) => void`
|
|
222
|
+
|
|
223
|
+
**Parameters:**
|
|
224
|
+
|
|
225
|
+
- `channel: Uint8Array`
|
|
226
|
+
- `delta: any` (optional)
|
|
227
|
+
|
|
228
|
+
#### socket.toString · [method](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L401)
|
|
229
|
+
|
|
230
|
+
**Signature:** `() => string`
|
|
231
|
+
|
|
232
|
+
**Parameters:**
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
#### socket.[Symbol.for('nodejs.util.inspect.custom')] · [method](https://github.com/vanviegen/edinburgh/blob/main/server/server.ts#L405)
|
|
236
|
+
|
|
237
|
+
**Signature:** `() => string`
|
|
238
|
+
|
|
239
|
+
**Parameters:**
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
## Client API Reference
|
|
243
|
+
|
|
244
|
+
The following is auto-generated from `client/client.ts`:
|
|
245
|
+
|
|
246
|
+
### logLevel · [variable](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L8)
|
|
247
|
+
|
|
248
|
+
Set to 1-3 for increasing verbosity.
|
|
249
|
+
|
|
250
|
+
**Value:** `number`
|
|
251
|
+
|
|
252
|
+
### ClientProxyObject · [type](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L157)
|
|
253
|
+
|
|
254
|
+
Transforms server-side API objects to client-side proxy objects with type-safe RPC methods.
|
|
255
|
+
|
|
256
|
+
**Type:** `{
|
|
257
|
+
[K in keyof T]: ClientProxyFunction<T[K]>
|
|
258
|
+
}`
|
|
259
|
+
|
|
260
|
+
### Connection · [class](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L190)
|
|
261
|
+
|
|
262
|
+
WebSocket connection to a Lowlander server with type-safe RPC, automatic reconnection,
|
|
263
|
+
and reactive updates.
|
|
264
|
+
|
|
265
|
+
**Type Parameters:**
|
|
266
|
+
|
|
267
|
+
- `T`
|
|
268
|
+
|
|
269
|
+
**Examples:**
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
import type * as API from './server/api.js';
|
|
273
|
+
const conn = new Connection<typeof API>('ws://localhost:8080/');
|
|
274
|
+
|
|
275
|
+
// Simple RPC - returns PromiseProxy
|
|
276
|
+
const sum = conn.api.add(1, 2);
|
|
277
|
+
|
|
278
|
+
// Server proxy for stateful APIs
|
|
279
|
+
const auth = conn.api.authenticate('token');
|
|
280
|
+
const secret = auth.serverProxy.getSecret();
|
|
281
|
+
|
|
282
|
+
// Streaming with callbacks
|
|
283
|
+
conn.api.streamData(data => console.log(data));
|
|
284
|
+
|
|
285
|
+
// Use within Aberdeen reactive scopes
|
|
286
|
+
$(() => {
|
|
287
|
+
dump(conn.isOnline());
|
|
288
|
+
dump(sum);
|
|
289
|
+
});
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
**Constructor Parameters:**
|
|
293
|
+
|
|
294
|
+
- `url`: - WebSocket URL (e.g., 'ws://localhost:8080/'), or a fake WebSocket object for testing
|
|
295
|
+
|
|
296
|
+
#### connection.ws · [property](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L192)
|
|
297
|
+
|
|
298
|
+
**Type:** `WebSocket`
|
|
299
|
+
|
|
300
|
+
#### connection.activeRequests · [property](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L193)
|
|
301
|
+
|
|
302
|
+
**Type:** `Map<number, ActiveRequest>`
|
|
303
|
+
|
|
304
|
+
#### connection.requestCounter · [property](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L194)
|
|
305
|
+
|
|
306
|
+
**Type:** `number`
|
|
307
|
+
|
|
308
|
+
#### connection.reconnectAttempts · [property](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L195)
|
|
309
|
+
|
|
310
|
+
**Type:** `number`
|
|
311
|
+
|
|
312
|
+
#### connection.onlineProxy · [property](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L198)
|
|
313
|
+
|
|
314
|
+
**Type:** `ValueRef<boolean>`
|
|
315
|
+
|
|
316
|
+
#### connection.api · [property](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L205)
|
|
317
|
+
|
|
318
|
+
Type-safe proxy to the server-side API. Methods return `PromiseProxy` objects
|
|
319
|
+
that work reactively in Aberdeen scopes. `ServerProxy` returns include a
|
|
320
|
+
`.serverProxy` property for accessing stateful server APIs.
|
|
321
|
+
|
|
322
|
+
**Type:** `ClientProxyObject<T>`
|
|
323
|
+
|
|
324
|
+
#### connection.isOnline · [method](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L218)
|
|
325
|
+
|
|
326
|
+
Returns the current connection status. Reactive in Aberdeen scopes.
|
|
327
|
+
|
|
328
|
+
**Signature:** `() => boolean`
|
|
329
|
+
|
|
330
|
+
**Parameters:**
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
#### connection.connect · [method](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L220)
|
|
334
|
+
|
|
335
|
+
**Signature:** `() => void`
|
|
336
|
+
|
|
337
|
+
**Parameters:**
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
#### connection.reconnect · [method](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L374)
|
|
341
|
+
|
|
342
|
+
**Signature:** `() => void`
|
|
343
|
+
|
|
344
|
+
**Parameters:**
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
#### connection.pruneCommitIds · [method](https://github.com/vanviegen/edinburgh/blob/main/client/client.ts#L388)
|
|
348
|
+
|
|
349
|
+
**Signature:** `(request: ActiveRequest, maxCommitId: number) => void`
|
|
350
|
+
|
|
351
|
+
**Parameters:**
|
|
352
|
+
|
|
353
|
+
- `request: ActiveRequest`
|
|
354
|
+
- `maxCommitId: number`
|
|
355
|
+
|
package/ROADMAP.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
Semi-prioritized Roadmap
|
|
2
|
+
========================
|
|
3
|
+
|
|
4
|
+
[ ] Integration with a component library
|
|
5
|
+
[/] Integrate a WarpSocket state inspection tool?
|
|
6
|
+
[ ] A simple built-in Sentry-like error logging system
|
|
7
|
+
[ ] Model-streaming with edit permissions for specific fields
|
|
8
|
+
[ ] Integration with an authentication library
|
|
9
|
+
[ ] Form builder based on Edinburgh schemas
|
|
10
|
+
[x] Database migrations
|
|
11
|
+
[ ] Streaming replication and backups
|
|
12
|
+
[ ] Federation primitives
|
|
13
|
+
[ ] Merge WarpSocket, Edinburgh and OLMDB into Lowlander, to allow model streaming and serialization to be done in Rust/Neon
|