lowlander 0.2.1 → 0.2.3
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 +288 -40
- package/build/client/client.d.ts +153 -0
- package/build/client/client.js +320 -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 +44 -0
- package/build/examples/helloworld/server/api.d.ts.map +1 -0
- package/build/examples/helloworld/server/api.js +163 -0
- 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/client/client.ts +9 -5
- package/package.json +16 -9
- package/server/server.ts +2 -1
- package/skill/Connection.md +35 -0
- package/skill/Connection_pruneCommitIds.md +8 -0
- package/skill/SKILL.md +430 -0
- package/skill/ServerProxy.md +30 -0
- package/skill/Socket.md +22 -0
- package/skill/Socket_send.md +11 -0
- package/skill/Socket_subscribe.md +8 -0
- package/skill/createStreamType.md +44 -0
- package/skill/pushModel.md +14 -0
- package/skill/sendModel.md +14 -0
- package/skill/start.md +21 -0
- package/skill/warpsocket.md +3 -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/examples/helloworld/server/api.ts +0 -154
- 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/tests/fake-warpsocket.ts
DELETED
|
@@ -1,452 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* In-process fake implementation of warpsocket for testing.
|
|
3
|
-
*
|
|
4
|
-
* Provides the full warpsocket API (send, subscribe, virtual sockets, KV store)
|
|
5
|
-
* backed by in-memory data structures. Message delivery uses setTimeout(fn, 0)
|
|
6
|
-
* so it integrates with Aberdeen's fakedom passTime().
|
|
7
|
-
*/
|
|
8
|
-
import * as pathMod from 'node:path';
|
|
9
|
-
|
|
10
|
-
import type { WorkerInterface } from 'warpsocket';
|
|
11
|
-
let workerModule: WorkerInterface;
|
|
12
|
-
|
|
13
|
-
interface VirtualSocket {
|
|
14
|
-
targetSocketId: number;
|
|
15
|
-
userPrefix?: Uint8Array;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface FakeClientSocket {
|
|
19
|
-
socketId: number;
|
|
20
|
-
onmessage?: ((event: {data: ArrayBuffer}) => void) | null;
|
|
21
|
-
onopen?: (() => void) | null;
|
|
22
|
-
onclose?: (() => void) | null;
|
|
23
|
-
onerror?: ((error: any) => void) | null;
|
|
24
|
-
readyState: number;
|
|
25
|
-
binaryType: string;
|
|
26
|
-
send(data: Uint8Array | ArrayBuffer): void;
|
|
27
|
-
close(): void;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
type ChannelDebug = { channel: Uint8Array, subscribers: { [socketId: number]: number } };
|
|
31
|
-
type SocketDebug = { ip: string, workerId: number } | { targetSocketId: number, userPrefix?: Uint8Array };
|
|
32
|
-
type WorkerDebug = { hasTextHandler: boolean, hasBinaryHandler: boolean, hasCloseHandler: boolean, hasOpenHandler: boolean };
|
|
33
|
-
type KVDebug = { key: Uint8Array, value: Uint8Array };
|
|
34
|
-
|
|
35
|
-
let nextSocketId = 1;
|
|
36
|
-
let nextVirtualSocketId = 100000; // High range to avoid collisions
|
|
37
|
-
let virtualSockets = new Map<number, VirtualSocket>();
|
|
38
|
-
let channels = new Map<string, Map<number, number>>(); // channelKey -> socketId -> refCount
|
|
39
|
-
let channelBytes = new Map<string, Uint8Array>(); // channelKey -> original channel name bytes
|
|
40
|
-
let kv = new Map<string, Uint8Array>();
|
|
41
|
-
let kvKeyBytes = new Map<string, Uint8Array>(); // keyStr -> original key bytes
|
|
42
|
-
let clientSockets = new Map<number, FakeClientSocket>();
|
|
43
|
-
|
|
44
|
-
/** Convert any accepted type to Uint8Array */
|
|
45
|
-
function toBytes(input: Uint8Array | ArrayBuffer | string): Uint8Array {
|
|
46
|
-
if (input instanceof Uint8Array) return input;
|
|
47
|
-
if (input instanceof ArrayBuffer) return new Uint8Array(input);
|
|
48
|
-
return new TextEncoder().encode(input);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function keyStr(key: Uint8Array | ArrayBuffer | string): string {
|
|
52
|
-
if (typeof key === 'string') return key;
|
|
53
|
-
const bytes = key instanceof Uint8Array ? key : new Uint8Array(key);
|
|
54
|
-
return Array.from(bytes).join(',');
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function channelKey(name: Uint8Array | ArrayBuffer | string): string {
|
|
58
|
-
return 'ch:' + keyStr(name);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export function send(target: number | number[] | Uint8Array | ArrayBuffer | string | (number | Uint8Array | ArrayBuffer | string)[], data: Uint8Array | ArrayBuffer | string): number {
|
|
62
|
-
const buf = toBytes(data);
|
|
63
|
-
|
|
64
|
-
if (typeof target === 'number') {
|
|
65
|
-
return sendToSocket(target, buf);
|
|
66
|
-
} else if (typeof target === 'string' || target instanceof Uint8Array || target instanceof ArrayBuffer) {
|
|
67
|
-
return sendToChannel(target, buf);
|
|
68
|
-
} else if (Array.isArray(target)) {
|
|
69
|
-
let count = 0;
|
|
70
|
-
for (const item of target) {
|
|
71
|
-
if (typeof item === 'number') {
|
|
72
|
-
count += sendToSocket(item, buf);
|
|
73
|
-
} else {
|
|
74
|
-
count += sendToChannel(item, buf);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
return count;
|
|
78
|
-
}
|
|
79
|
-
return 0;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function sendToSocket(id: number, data: Uint8Array): number {
|
|
83
|
-
const vs = virtualSockets.get(id);
|
|
84
|
-
if (vs) {
|
|
85
|
-
let finalData: Uint8Array;
|
|
86
|
-
if (vs.userPrefix) {
|
|
87
|
-
finalData = new Uint8Array(vs.userPrefix.length + data.length);
|
|
88
|
-
finalData.set(vs.userPrefix, 0);
|
|
89
|
-
finalData.set(data, vs.userPrefix.length);
|
|
90
|
-
} else {
|
|
91
|
-
finalData = data;
|
|
92
|
-
}
|
|
93
|
-
return sendToSocket(vs.targetSocketId, finalData);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const client = clientSockets.get(id);
|
|
97
|
-
if (!client || client.readyState !== 1) return 0;
|
|
98
|
-
|
|
99
|
-
const copy = new Uint8Array(data).slice();
|
|
100
|
-
setTimeout(() => {
|
|
101
|
-
if (client.readyState === 1 && client.onmessage) {
|
|
102
|
-
client.onmessage({data: copy.buffer});
|
|
103
|
-
}
|
|
104
|
-
}, 0);
|
|
105
|
-
return 1;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function sendToChannel(channelName: Uint8Array | ArrayBuffer | string, data: Uint8Array): number {
|
|
109
|
-
const key = channelKey(channelName);
|
|
110
|
-
const subscribers = channels.get(key);
|
|
111
|
-
if (!subscribers) return 0;
|
|
112
|
-
let count = 0;
|
|
113
|
-
for (const vsId of subscribers.keys()) {
|
|
114
|
-
count += sendToSocket(vsId, data);
|
|
115
|
-
}
|
|
116
|
-
return count;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Subscribes one or more WebSocket connections to a channel, or copies subscriptions from one channel to another.
|
|
121
|
-
* Multiple subscriptions to the same channel by the same connection are reference-counted.
|
|
122
|
-
*
|
|
123
|
-
* @param socketIdOrChannelName - Can be:
|
|
124
|
-
* - A single socket ID (number): applies delta to that socket's subscription
|
|
125
|
-
* - An array of socket IDs (number[]): applies delta to all sockets' subscriptions
|
|
126
|
-
* - A channel name (Buffer/ArrayBuffer/string): applies delta to all subscribers of this source channel
|
|
127
|
-
* - An array mixing socket IDs and channel names: applies delta to sockets and source channel subscribers
|
|
128
|
-
* @param channelName - The target channel name (Buffer, ArrayBuffer, or string).
|
|
129
|
-
* @param delta - Optional. The amount to change the subscription count by (default: 1).
|
|
130
|
-
* Positive values add subscriptions, negative values remove them. When the count reaches zero, the subscription is removed.
|
|
131
|
-
* @returns An array of socket IDs that were affected by the operation:
|
|
132
|
-
* - For positive delta: socket IDs that became newly subscribed (reference count went from 0 to positive)
|
|
133
|
-
* - For negative delta: socket IDs that became completely unsubscribed (reference count reached 0)
|
|
134
|
-
*/
|
|
135
|
-
export function subscribe(target: number | number[] | Uint8Array | ArrayBuffer | string | (number | Uint8Array | ArrayBuffer | string)[], channelName: Uint8Array | ArrayBuffer | string, delta: number = 1): number[] {
|
|
136
|
-
const key = channelKey(channelName);
|
|
137
|
-
let subscribers = channels.get(key);
|
|
138
|
-
if (!subscribers) {
|
|
139
|
-
channels.set(key, subscribers = new Map());
|
|
140
|
-
channelBytes.set(key, toBytes(channelName));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// Resolve targets to socket IDs
|
|
144
|
-
let socketIds: number[];
|
|
145
|
-
if (typeof target === 'number') {
|
|
146
|
-
socketIds = [target];
|
|
147
|
-
} else if (Array.isArray(target)) {
|
|
148
|
-
socketIds = [];
|
|
149
|
-
for (const item of target) {
|
|
150
|
-
if (typeof item === 'number') {
|
|
151
|
-
socketIds.push(item);
|
|
152
|
-
} else {
|
|
153
|
-
// Channel name — gather its subscribers
|
|
154
|
-
const srcSubs = channels.get(channelKey(item));
|
|
155
|
-
if (srcSubs) for (const sid of srcSubs.keys()) socketIds.push(sid);
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
} else {
|
|
159
|
-
// target is a channel name (Uint8Array | ArrayBuffer | string)
|
|
160
|
-
const srcSubs = channels.get(channelKey(target));
|
|
161
|
-
socketIds = srcSubs ? [...srcSubs.keys()] : [];
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const changed: number[] = [];
|
|
165
|
-
for (const sid of socketIds) {
|
|
166
|
-
const current = subscribers.get(sid) || 0;
|
|
167
|
-
const next = current + delta;
|
|
168
|
-
if (delta > 0) {
|
|
169
|
-
subscribers.set(sid, next);
|
|
170
|
-
if (current === 0) changed.push(sid);
|
|
171
|
-
} else {
|
|
172
|
-
if (next <= 0) {
|
|
173
|
-
subscribers.delete(sid);
|
|
174
|
-
if (current > 0) changed.push(sid);
|
|
175
|
-
} else {
|
|
176
|
-
subscribers.set(sid, next);
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
if (!subscribers.size) {
|
|
181
|
-
channels.delete(key);
|
|
182
|
-
channelBytes.delete(key);
|
|
183
|
-
}
|
|
184
|
-
return changed;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Exactly the same as `subscribe`, only with a negative delta (defaulting to 1, which means a single unsubscribe, or a subscribe with delta -1).
|
|
189
|
-
*/
|
|
190
|
-
export function unsubscribe(socketIdOrChannelName: number | number[] | Uint8Array | ArrayBuffer | string | (number | Uint8Array | ArrayBuffer | string)[], channelName: Uint8Array | ArrayBuffer | string, delta: number = 1): number[] {
|
|
191
|
-
return subscribe(socketIdOrChannelName, channelName, -delta);
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* **DEPRECATED:** Use subscribe(fromChannelName, toChannelName) instead.
|
|
196
|
-
*
|
|
197
|
-
* Copies all subscribers from one channel to another channel. Uses reference counting - if a subscriber
|
|
198
|
-
* is already subscribed to the destination channel, their reference count will be incremented instead
|
|
199
|
-
* of creating duplicate subscriptions.
|
|
200
|
-
* @param fromChannelName - The source channel name (Buffer, ArrayBuffer, or string).
|
|
201
|
-
* @param toChannelName - The destination channel name (Buffer, ArrayBuffer, or string).
|
|
202
|
-
* @returns An array of socket IDs that were newly added to the destination channel. Sockets that were
|
|
203
|
-
* already subscribed (and had their reference count incremented) are not included.
|
|
204
|
-
*/
|
|
205
|
-
export function copySubscriptions(fromChannelName: Uint8Array | ArrayBuffer | string, toChannelName: Uint8Array | ArrayBuffer | string): number[] {
|
|
206
|
-
return subscribe(fromChannelName, toChannelName);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export function hasSubscriptions(channelName: Uint8Array | ArrayBuffer | string): boolean {
|
|
210
|
-
const key = channelKey(channelName);
|
|
211
|
-
const subscribers = channels.get(key);
|
|
212
|
-
return !!subscribers && subscribers.size > 0;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export function createVirtualSocket(socketId: number, userPrefix?: Uint8Array | ArrayBuffer | string): number {
|
|
216
|
-
const vsId = nextVirtualSocketId++;
|
|
217
|
-
virtualSockets.set(vsId, {
|
|
218
|
-
targetSocketId: socketId,
|
|
219
|
-
userPrefix: userPrefix != null ? toBytes(userPrefix) : undefined,
|
|
220
|
-
});
|
|
221
|
-
return vsId;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
export function deleteVirtualSocket(virtualSocketId: number, expectedTargetSocketId?: number): boolean {
|
|
225
|
-
const vs = virtualSockets.get(virtualSocketId);
|
|
226
|
-
if (!vs) return false;
|
|
227
|
-
if (expectedTargetSocketId !== undefined && vs.targetSocketId !== expectedTargetSocketId) return false;
|
|
228
|
-
virtualSockets.delete(virtualSocketId);
|
|
229
|
-
// Unsubscribe from all channels
|
|
230
|
-
for (const [key, subscribers] of channels) {
|
|
231
|
-
subscribers.delete(virtualSocketId);
|
|
232
|
-
if (!subscribers.size) {
|
|
233
|
-
channels.delete(key);
|
|
234
|
-
channelBytes.delete(key);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
return true;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
export function getKey(key: Uint8Array | ArrayBuffer | string): Uint8Array | undefined {
|
|
241
|
-
return kv.get(keyStr(key));
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
export function setKey(key: Uint8Array | ArrayBuffer | string, value?: Uint8Array | ArrayBuffer | string): Uint8Array | undefined {
|
|
245
|
-
const k = keyStr(key);
|
|
246
|
-
const prev = kv.get(k);
|
|
247
|
-
if (value == null) {
|
|
248
|
-
kv.delete(k);
|
|
249
|
-
kvKeyBytes.delete(k);
|
|
250
|
-
} else {
|
|
251
|
-
kv.set(k, toBytes(value));
|
|
252
|
-
if (!kvKeyBytes.has(k)) kvKeyBytes.set(k, toBytes(key));
|
|
253
|
-
}
|
|
254
|
-
return prev;
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
export function setKeyIf(key: Uint8Array | ArrayBuffer | string, newValue?: Uint8Array | ArrayBuffer | string, checkValue?: Uint8Array | ArrayBuffer | string): boolean {
|
|
258
|
-
const k = keyStr(key);
|
|
259
|
-
const current = kv.get(k);
|
|
260
|
-
const checkBytes = checkValue != null ? toBytes(checkValue) : undefined;
|
|
261
|
-
// Compare: both undefined, or byte-equal
|
|
262
|
-
const matches = (current === undefined && checkBytes === undefined) || (
|
|
263
|
-
current !== undefined && checkBytes !== undefined &&
|
|
264
|
-
current.length === checkBytes.length &&
|
|
265
|
-
current.every((v, i) => v === checkBytes[i])
|
|
266
|
-
);
|
|
267
|
-
if (!matches) return false;
|
|
268
|
-
if (newValue == null) {
|
|
269
|
-
kv.delete(k);
|
|
270
|
-
kvKeyBytes.delete(k);
|
|
271
|
-
} else {
|
|
272
|
-
kv.set(k, toBytes(newValue));
|
|
273
|
-
if (!kvKeyBytes.has(k)) kvKeyBytes.set(k, toBytes(key));
|
|
274
|
-
}
|
|
275
|
-
return true;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
export function getDebugState(mode: "channels"): ChannelDebug[];
|
|
279
|
-
export function getDebugState(mode: "channels", channelName: Uint8Array | ArrayBuffer | string): ChannelDebug | undefined;
|
|
280
|
-
export function getDebugState(mode: "channels", filterSocketId: number): ChannelDebug[];
|
|
281
|
-
export function getDebugState(mode: "sockets"): Record<number, SocketDebug>;
|
|
282
|
-
export function getDebugState(mode: "sockets", socketId: number): SocketDebug | undefined;
|
|
283
|
-
export function getDebugState(mode: "workers"): Record<number, WorkerDebug>;
|
|
284
|
-
export function getDebugState(mode: "workers", workerId: number): WorkerDebug | undefined;
|
|
285
|
-
export function getDebugState(mode: "kv"): KVDebug[];
|
|
286
|
-
export function getDebugState(mode: string, singleKey?: any): any {
|
|
287
|
-
if (mode === 'channels') {
|
|
288
|
-
if (singleKey !== undefined) {
|
|
289
|
-
if (typeof singleKey === 'number') {
|
|
290
|
-
// Filter: all channels this socket is subscribed to
|
|
291
|
-
const result: ChannelDebug[] = [];
|
|
292
|
-
for (const [key, subscribers] of channels) {
|
|
293
|
-
if (subscribers.has(singleKey)) {
|
|
294
|
-
const subs: { [id: number]: number } = {};
|
|
295
|
-
for (const [sid, count] of subscribers) subs[sid] = count;
|
|
296
|
-
result.push({ channel: channelBytes.get(key)!, subscribers: subs });
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
return result;
|
|
300
|
-
} else {
|
|
301
|
-
// Lookup single channel by name
|
|
302
|
-
const key = channelKey(singleKey);
|
|
303
|
-
const subscribers = channels.get(key);
|
|
304
|
-
if (!subscribers) return undefined;
|
|
305
|
-
const subs: { [id: number]: number } = {};
|
|
306
|
-
for (const [sid, count] of subscribers) subs[sid] = count;
|
|
307
|
-
return { channel: channelBytes.get(key)!, subscribers: subs } as ChannelDebug;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
const result: ChannelDebug[] = [];
|
|
311
|
-
for (const [key, subscribers] of channels) {
|
|
312
|
-
const subs: { [id: number]: number } = {};
|
|
313
|
-
for (const [sid, count] of subscribers) subs[sid] = count;
|
|
314
|
-
result.push({ channel: channelBytes.get(key)!, subscribers: subs });
|
|
315
|
-
}
|
|
316
|
-
return result;
|
|
317
|
-
}
|
|
318
|
-
if (mode === 'sockets') {
|
|
319
|
-
if (typeof singleKey === 'number') {
|
|
320
|
-
const vs = virtualSockets.get(singleKey);
|
|
321
|
-
if (vs) {
|
|
322
|
-
const debug: any = { targetSocketId: vs.targetSocketId };
|
|
323
|
-
if (vs.userPrefix) debug.userPrefix = vs.userPrefix;
|
|
324
|
-
return debug as SocketDebug;
|
|
325
|
-
}
|
|
326
|
-
const cs = clientSockets.get(singleKey);
|
|
327
|
-
if (cs) return { ip: '127.0.0.1', workerId: 0 } as SocketDebug;
|
|
328
|
-
return undefined;
|
|
329
|
-
}
|
|
330
|
-
const result: Record<number, SocketDebug> = {};
|
|
331
|
-
for (const [id] of clientSockets) {
|
|
332
|
-
result[id] = { ip: '127.0.0.1', workerId: 0 };
|
|
333
|
-
}
|
|
334
|
-
for (const [id, vs] of virtualSockets) {
|
|
335
|
-
const debug: any = { targetSocketId: vs.targetSocketId };
|
|
336
|
-
if (vs.userPrefix) debug.userPrefix = vs.userPrefix;
|
|
337
|
-
result[id] = debug;
|
|
338
|
-
}
|
|
339
|
-
return result;
|
|
340
|
-
}
|
|
341
|
-
if (mode === 'workers') {
|
|
342
|
-
const info: WorkerDebug = {
|
|
343
|
-
hasTextHandler: !!workerModule?.handleTextMessage,
|
|
344
|
-
hasBinaryHandler: !!workerModule?.handleBinaryMessage,
|
|
345
|
-
hasCloseHandler: !!workerModule?.handleClose,
|
|
346
|
-
hasOpenHandler: !!workerModule?.handleOpen,
|
|
347
|
-
};
|
|
348
|
-
if (typeof singleKey === 'number') return singleKey === 0 ? info : undefined;
|
|
349
|
-
return { 0: info } as Record<number, WorkerDebug>;
|
|
350
|
-
}
|
|
351
|
-
if (mode === 'kv') {
|
|
352
|
-
const result: KVDebug[] = [];
|
|
353
|
-
for (const [k, v] of kv) {
|
|
354
|
-
result.push({ key: kvKeyBytes.get(k) || toBytes(k), value: v });
|
|
355
|
-
}
|
|
356
|
-
return result;
|
|
357
|
-
}
|
|
358
|
-
return {};
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/** Create a fake client-side WebSocket that's wired to this in-process server. */
|
|
362
|
-
export function createClientSocket(): WebSocket {
|
|
363
|
-
const socketId = nextSocketId++;
|
|
364
|
-
|
|
365
|
-
// Serialize message processing per socket (like real warpsocket)
|
|
366
|
-
let messageQueue: Promise<void> = Promise.resolve();
|
|
367
|
-
|
|
368
|
-
const socket: FakeClientSocket = {
|
|
369
|
-
socketId,
|
|
370
|
-
onmessage: null,
|
|
371
|
-
onopen: null,
|
|
372
|
-
onclose: null,
|
|
373
|
-
onerror: null,
|
|
374
|
-
readyState: 0, // CONNECTING
|
|
375
|
-
binaryType: 'arraybuffer',
|
|
376
|
-
|
|
377
|
-
send(data: Uint8Array | ArrayBuffer) {
|
|
378
|
-
const buf = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
379
|
-
// Queue message processing to ensure sequential handling
|
|
380
|
-
messageQueue = messageQueue.then(() => new Promise<void>(resolve => {
|
|
381
|
-
setTimeout(async () => {
|
|
382
|
-
try {
|
|
383
|
-
await workerModule.handleBinaryMessage?.(buf, socketId);
|
|
384
|
-
} catch (e) {
|
|
385
|
-
console.error('FakeWarpSocket: handleBinaryMessage error', e);
|
|
386
|
-
}
|
|
387
|
-
resolve();
|
|
388
|
-
}, 0);
|
|
389
|
-
}));
|
|
390
|
-
},
|
|
391
|
-
|
|
392
|
-
close() {
|
|
393
|
-
if (socket.readyState >= 2) return;
|
|
394
|
-
socket.readyState = 2; // CLOSING
|
|
395
|
-
clientSockets.delete(socketId);
|
|
396
|
-
setTimeout(() => {
|
|
397
|
-
socket.readyState = 3; // CLOSED
|
|
398
|
-
workerModule.handleClose?.(socketId);
|
|
399
|
-
socket.onclose?.();
|
|
400
|
-
}, 0);
|
|
401
|
-
},
|
|
402
|
-
};
|
|
403
|
-
|
|
404
|
-
clientSockets.set(socketId, socket);
|
|
405
|
-
|
|
406
|
-
// Open asynchronously
|
|
407
|
-
setTimeout(() => {
|
|
408
|
-
socket.readyState = 1; // OPEN
|
|
409
|
-
workerModule.handleOpen?.(socketId, '127.0.0.1', {"user-agent": "FakeWarpSocket"});
|
|
410
|
-
socket.onopen?.();
|
|
411
|
-
}, 0);
|
|
412
|
-
|
|
413
|
-
return socket as any;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/** Reset all state (for between tests). */
|
|
417
|
-
export function reset() {
|
|
418
|
-
for (const client of clientSockets.values()) {
|
|
419
|
-
client.readyState = 3;
|
|
420
|
-
}
|
|
421
|
-
clientSockets.clear();
|
|
422
|
-
virtualSockets.clear();
|
|
423
|
-
channels.clear();
|
|
424
|
-
channelBytes.clear();
|
|
425
|
-
kv.clear();
|
|
426
|
-
kvKeyBytes.clear();
|
|
427
|
-
nextSocketId = 1;
|
|
428
|
-
nextVirtualSocketId = 100000;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Initializes the fake warpsocket. Ignores `threads` and `bind`.
|
|
433
|
-
*/
|
|
434
|
-
|
|
435
|
-
export async function start(options: {
|
|
436
|
-
bind: string | string[];
|
|
437
|
-
workerPath?: string;
|
|
438
|
-
threads?: number;
|
|
439
|
-
workerArg?: any;
|
|
440
|
-
}): Promise<void> {
|
|
441
|
-
if (!options || !options.bind || !options.workerPath) {
|
|
442
|
-
throw new Error('options.bind and options.workerPath are required');
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
if (!pathMod.isAbsolute(options.workerPath)) {
|
|
446
|
-
options.workerPath = pathMod.resolve(process.cwd(), options.workerPath);
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
workerModule = await import(options.workerPath);
|
|
450
|
-
await workerModule.handleStart?.(options.workerArg);
|
|
451
|
-
}
|
|
452
|
-
|
package/tests/helloworld.test.ts
DELETED
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { expect, test, beforeAll, afterEach, beforeEach } from "bun:test";
|
|
2
|
-
import { passTime, assertBody, reset as resetAberdeen } from "aberdeen/test-helpers";
|
|
3
|
-
import * as E from "edinburgh";
|
|
4
|
-
import { start } from "lowlander/server";
|
|
5
|
-
import { Connection } from "lowlander/client";
|
|
6
|
-
import type * as API from "../examples/helloworld/server/api.js";
|
|
7
|
-
import A from "aberdeen";
|
|
8
|
-
import * as fakeWarpSocket from "./fake-warpsocket.js";
|
|
9
|
-
|
|
10
|
-
beforeAll(async () => {
|
|
11
|
-
E.init('.edinburgh_test');
|
|
12
|
-
E.setMaxRetryCount(100);
|
|
13
|
-
await start(
|
|
14
|
-
new URL('../examples/helloworld/server/api.ts', import.meta.url).pathname,
|
|
15
|
-
{ injectWarpSocket: fakeWarpSocket },
|
|
16
|
-
);
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
beforeEach(async () => {
|
|
20
|
-
await connect().api.resetTestData(true).promise;
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
afterEach(async () => {
|
|
24
|
-
fakeWarpSocket.reset();
|
|
25
|
-
A.unmountAll();
|
|
26
|
-
await resetAberdeen();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
function connect() {
|
|
30
|
-
return new Connection<typeof API>(fakeWarpSocket.createClientSocket);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
test('simple RPC: add', async () => {
|
|
34
|
-
const c = connect();
|
|
35
|
-
const sum = c.api.add(2, 3);
|
|
36
|
-
await passTime();
|
|
37
|
-
expect(sum.value).toBe(5);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test('authenticate returns ServerProxy', async () => {
|
|
41
|
-
const c = connect();
|
|
42
|
-
const auth = c.api.authenticate('Frank');
|
|
43
|
-
await passTime(1100);
|
|
44
|
-
expect(auth.value).toBe('secret');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test('authenticate with unknown user throws', async () => {
|
|
48
|
-
const c = connect();
|
|
49
|
-
const auth = c.api.authenticate('Nobody');
|
|
50
|
-
auth.promise.catch(() => {}); // prevent unhandled rejection
|
|
51
|
-
await passTime(1100);
|
|
52
|
-
expect(auth.error).toBeDefined();
|
|
53
|
-
expect(auth.error.message).toBe('User not found');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
test('ServerProxy: getBio', async () => {
|
|
57
|
-
const c = connect();
|
|
58
|
-
const auth = c.api.authenticate('Frank');
|
|
59
|
-
const bio = auth.serverProxy.getBio();
|
|
60
|
-
await passTime(1100);
|
|
61
|
-
expect(bio.value).toContain('Frank is 45 years old');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
test('ServerProxy: toggleFriend', async () => {
|
|
65
|
-
const c = connect();
|
|
66
|
-
const auth = c.api.authenticate('Frank');
|
|
67
|
-
const result = auth.serverProxy.toggleFriend('Alice');
|
|
68
|
-
await passTime(1100);
|
|
69
|
-
expect(typeof result.value).toBe('boolean');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test('model streaming: streamModel', async () => {
|
|
73
|
-
const c = connect();
|
|
74
|
-
const model = c.api.streamModel();
|
|
75
|
-
await passTime();
|
|
76
|
-
expect(model.value).toBeDefined();
|
|
77
|
-
expect(model.value!.name).toBe('Test');
|
|
78
|
-
expect(model.value!.owner).toBeDefined();
|
|
79
|
-
expect(model.value!.owner.name).toBe('Frank');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test('two clients see same RPC result', async () => {
|
|
83
|
-
const c1 = connect();
|
|
84
|
-
const c2 = connect();
|
|
85
|
-
const sum1 = c1.api.add(10, 20);
|
|
86
|
-
const sum2 = c2.api.add(10, 20);
|
|
87
|
-
await passTime();
|
|
88
|
-
expect(sum1.value).toBe(30);
|
|
89
|
-
expect(sum2.value).toBe(30);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test('online status', async () => {
|
|
93
|
-
const c = connect();
|
|
94
|
-
expect(c.isOnline()).toBe(false);
|
|
95
|
-
await passTime();
|
|
96
|
-
expect(c.isOnline()).toBe(true);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test('render RPC result in DOM', async () => {
|
|
100
|
-
const c = connect();
|
|
101
|
-
const sum = c.api.add(7, 8);
|
|
102
|
-
A(() => {
|
|
103
|
-
if (sum.value !== undefined) A('span#' + sum.value);
|
|
104
|
-
});
|
|
105
|
-
assertBody('');
|
|
106
|
-
await passTime();
|
|
107
|
-
expect(sum.value).toBe(15);
|
|
108
|
-
await passTime();
|
|
109
|
-
assertBody('span{"15"}');
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test('two connections render in same DOM', async () => {
|
|
113
|
-
const c1 = connect();
|
|
114
|
-
const c2 = connect();
|
|
115
|
-
const s1 = c1.api.add(1, 2);
|
|
116
|
-
const s2 = c2.api.add(3, 4);
|
|
117
|
-
A('div', () => {
|
|
118
|
-
A(() => {
|
|
119
|
-
if (s1.value !== undefined) A('span.c1#' + s1.value);
|
|
120
|
-
});
|
|
121
|
-
A(() => {
|
|
122
|
-
if (s2.value !== undefined) A('span.c2#' + s2.value);
|
|
123
|
-
});
|
|
124
|
-
});
|
|
125
|
-
await passTime();
|
|
126
|
-
assertBody('div{span.c1{"3"} span.c2{"7"}}');
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test('4 connections stream, mutate, and converge', async () => {
|
|
130
|
-
const conns = Array.from({length: 4}, () => connect());
|
|
131
|
-
const models = conns.map(c => c.api.streamModel());
|
|
132
|
-
A('div', () => {
|
|
133
|
-
for (const [i, m] of models.entries()) {
|
|
134
|
-
A(() => {
|
|
135
|
-
if (m.value) A(`span.c${i}#` + m.value.owner.age);
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
await passTime();
|
|
140
|
-
conns[0].api.setOwnerAge(100);
|
|
141
|
-
await passTime();
|
|
142
|
-
// All 4 connections increment simultaneously, racing through retries
|
|
143
|
-
await Promise.all(conns.map((c, i) => c.api.incrOwnerAge(i + 1).promise));
|
|
144
|
-
// Await UI update
|
|
145
|
-
await passTime();
|
|
146
|
-
// All 4 should converge; total increment = 1+2+3+4 = 10, so age = 110
|
|
147
|
-
assertBody('div{span.c0{"110"} span.c1{"110"} span.c2{"110"} span.c3{"110"}}');
|
|
148
|
-
// Restore original age
|
|
149
|
-
conns[0].api.setOwnerAge(45);
|
|
150
|
-
await passTime();
|
|
151
|
-
});
|
package/tsconfig.client.json
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "./tsconfig.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"composite": true,
|
|
5
|
-
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
-
"outDir": "./build",
|
|
7
|
-
"rootDir": ".",
|
|
8
|
-
"paths": {
|
|
9
|
-
"lowlander/client": ["./client/client.ts"],
|
|
10
|
-
"lowlander/server": ["./server/server.ts"]
|
|
11
|
-
}
|
|
12
|
-
},
|
|
13
|
-
"include": ["client/**/*.ts", "examples/**/client/**/*.ts"],
|
|
14
|
-
"exclude": ["node_modules", "build", "tests"],
|
|
15
|
-
"references": [
|
|
16
|
-
{"path": "./tsconfig.server.json"}
|
|
17
|
-
]
|
|
18
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "es2022",
|
|
4
|
-
"experimentalDecorators": true,
|
|
5
|
-
"module": "ESNext",
|
|
6
|
-
"lib": ["ES2022"],
|
|
7
|
-
"skipLibCheck": true,
|
|
8
|
-
"moduleResolution": "bundler",
|
|
9
|
-
"resolveJsonModule": true,
|
|
10
|
-
"isolatedModules": true,
|
|
11
|
-
"sourceMap": true,
|
|
12
|
-
"strict": true,
|
|
13
|
-
"noUnusedLocals": true,
|
|
14
|
-
"noUnusedParameters": true,
|
|
15
|
-
"noFallthroughCasesInSwitch": true
|
|
16
|
-
},
|
|
17
|
-
"references": [
|
|
18
|
-
{ "path": "./tsconfig.server.json" },
|
|
19
|
-
{ "path": "./tsconfig.client.json" },
|
|
20
|
-
{ "path": "./tsconfig.test.json" }
|
|
21
|
-
],
|
|
22
|
-
"files": [],
|
|
23
|
-
"exclude": ["node_modules", "build"]
|
|
24
|
-
}
|
package/tsconfig.server.json
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "./tsconfig.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"composite": true,
|
|
5
|
-
"lib": ["ES2020", "ESNext.WeakRef"],
|
|
6
|
-
"types": ["bun"],
|
|
7
|
-
"declaration": true,
|
|
8
|
-
"declarationMap": true,
|
|
9
|
-
"outDir": "./build",
|
|
10
|
-
"rootDir": ".",
|
|
11
|
-
"paths": {
|
|
12
|
-
"lowlander/server": ["./server/server.ts"]
|
|
13
|
-
}
|
|
14
|
-
},
|
|
15
|
-
"include": ["server/**/*.ts", "examples/**/server/**/*.ts", "tests/fake-warpsocket.ts"],
|
|
16
|
-
"exclude": ["node_modules", "build", "tests"]
|
|
17
|
-
}
|
package/tsconfig.test.json
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "./tsconfig.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
5
|
-
"types": ["bun"],
|
|
6
|
-
"noEmit": true,
|
|
7
|
-
"paths": {
|
|
8
|
-
"lowlander/server": ["./server/server.ts"],
|
|
9
|
-
"lowlander/client": ["./client/client.ts"]
|
|
10
|
-
}
|
|
11
|
-
},
|
|
12
|
-
"include": ["tests/**/*.ts"]
|
|
13
|
-
}
|
|
File without changes
|
|
File without changes
|