@bloopjs/bloop 0.0.20 → 0.0.22
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/dist/bloop.d.ts +52 -0
- package/dist/bloop.d.ts.map +1 -0
- package/dist/context.d.ts +17 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/data/bag.d.ts +8 -0
- package/dist/data/bag.d.ts.map +1 -0
- package/dist/data/schema.d.ts +5 -0
- package/dist/data/schema.d.ts.map +1 -0
- package/dist/events.d.ts +17 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/mod.d.ts +8 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +1290 -0
- package/dist/mod.js.map +14 -0
- package/dist/mount.d.ts +17 -0
- package/dist/mount.d.ts.map +1 -0
- package/dist/runtime.d.ts +60 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/system.d.ts +26 -0
- package/dist/system.d.ts.map +1 -0
- package/dist/util.d.ts +4 -0
- package/dist/util.d.ts.map +1 -0
- package/package.json +10 -2
- package/src/bloop.ts +145 -129
- package/src/mod.ts +8 -3
- package/src/mount.ts +86 -0
- package/src/runtime.ts +15 -94
- package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +0 -111
- package/jsr.json +0 -6
- package/publish/tsconfig.publish.json +0 -7
- package/test/inputs.test.ts +0 -227
- package/test/runtime.test.ts +0 -298
- package/test/tape.test.ts +0 -128
- package/tsconfig.json +0 -34
package/src/bloop.ts
CHANGED
|
@@ -21,7 +21,7 @@ import type {
|
|
|
21
21
|
MouseMoveEvent,
|
|
22
22
|
MouseWheelEvent,
|
|
23
23
|
} from "./events";
|
|
24
|
-
import type { DeserializeFn, SerializeFn } from "./runtime";
|
|
24
|
+
import type { DeserializeFn, EngineHooks, SerializeFn } from "./runtime";
|
|
25
25
|
import type { System } from "./system";
|
|
26
26
|
|
|
27
27
|
export type BloopOpts<B extends Bag> = {
|
|
@@ -63,6 +63,9 @@ export class Bloop<GS extends BloopSchema> {
|
|
|
63
63
|
return new Bloop<MakeGS<B>>(opts, "dontCallMeDirectly");
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* DO NOT USE `new Bloop` - use `Bloop.create()` instead for proper type hints.
|
|
68
|
+
*/
|
|
66
69
|
constructor(opts: BloopOpts<GS["B"]> = {}, dontCallMeDirectly: string) {
|
|
67
70
|
if (dontCallMeDirectly !== "dontCallMeDirectly") {
|
|
68
71
|
throw new Error(
|
|
@@ -78,51 +81,22 @@ export class Bloop<GS extends BloopSchema> {
|
|
|
78
81
|
};
|
|
79
82
|
}
|
|
80
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Read the game singleton bag
|
|
86
|
+
*/
|
|
81
87
|
get bag(): GS["B"] {
|
|
82
88
|
return this.#context.bag;
|
|
83
89
|
}
|
|
84
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Read the game context object
|
|
93
|
+
*/
|
|
85
94
|
get context(): Readonly<Context<GS>> {
|
|
86
95
|
return this.#context;
|
|
87
96
|
}
|
|
88
97
|
|
|
89
|
-
/**
|
|
90
|
-
* Take a snapshot of the game state outside the engine
|
|
91
|
-
* @returns linear memory representation of the game state that the engine is unaware of
|
|
92
|
-
*/
|
|
93
|
-
serialize = (): ReturnType<SerializeFn> => {
|
|
94
|
-
const str = JSON.stringify(this.#context.bag);
|
|
95
|
-
const encoder = new TextEncoder();
|
|
96
|
-
const textBytes = encoder.encode(str);
|
|
97
|
-
|
|
98
|
-
return {
|
|
99
|
-
size: textBytes.byteLength,
|
|
100
|
-
write: (buffer: ArrayBufferLike, ptr: EnginePointer) => {
|
|
101
|
-
const memoryView = new Uint8Array(buffer, ptr, textBytes.byteLength);
|
|
102
|
-
memoryView.set(textBytes);
|
|
103
|
-
},
|
|
104
|
-
};
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Restore a snapshot of the game state outside the engine
|
|
109
|
-
* @returns linear memory representation of the game state that the engine is unaware of
|
|
110
|
-
*/
|
|
111
|
-
deserialize: DeserializeFn = (buffer, ptr, len) => {
|
|
112
|
-
const bagBytes = new Uint8Array(buffer, ptr, len);
|
|
113
|
-
const decoder = new TextDecoder();
|
|
114
|
-
const str = decoder.decode(bagBytes);
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
this.#context.bag = JSON.parse(str);
|
|
118
|
-
} catch (e) {
|
|
119
|
-
console.error("failed to deserialize bag", { json: str, error: e });
|
|
120
|
-
}
|
|
121
|
-
};
|
|
122
|
-
|
|
123
98
|
/**
|
|
124
99
|
* Register a system with the game loop.
|
|
125
|
-
*
|
|
126
100
|
*/
|
|
127
101
|
system(label: string, system: System<GS>): number {
|
|
128
102
|
system.label ??= label;
|
|
@@ -130,102 +104,144 @@ export class Bloop<GS extends BloopSchema> {
|
|
|
130
104
|
return this.#systems.length;
|
|
131
105
|
}
|
|
132
106
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
this.#
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Low level hooks to engine functionality. Editing these is for advanced use cases, defaults should usually work.
|
|
109
|
+
*/
|
|
110
|
+
hooks: EngineHooks = {
|
|
111
|
+
/**
|
|
112
|
+
* Take a snapshot of the game state outside the engine
|
|
113
|
+
* @returns linear memory representation of the game state that the engine is unaware of
|
|
114
|
+
*/
|
|
115
|
+
serialize: () => {
|
|
116
|
+
const str = JSON.stringify(this.#context.bag);
|
|
117
|
+
const encoder = new TextEncoder();
|
|
118
|
+
const textBytes = encoder.encode(str);
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
size: textBytes.byteLength,
|
|
122
|
+
write: (buffer: ArrayBufferLike, ptr: EnginePointer) => {
|
|
123
|
+
const memoryView = new Uint8Array(buffer, ptr, textBytes.byteLength);
|
|
124
|
+
memoryView.set(textBytes);
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Restore a snapshot of the game state outside the engine
|
|
131
|
+
* @returns linear memory representation of the game state that the engine is unaware of
|
|
132
|
+
*/
|
|
133
|
+
deserialize: (buffer, ptr, len) => {
|
|
134
|
+
const bagBytes = new Uint8Array(buffer, ptr, len);
|
|
135
|
+
const decoder = new TextDecoder();
|
|
136
|
+
const str = decoder.decode(bagBytes);
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
this.#context.bag = JSON.parse(str);
|
|
140
|
+
} catch (e) {
|
|
141
|
+
console.error("failed to deserialize bag", { json: str, error: e });
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
setBuffer: (buffer: ArrayBuffer) => {
|
|
146
|
+
this.#engineBuffer = buffer;
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
systemsCallback: (system_handle: number, ptr: EnginePointer) => {
|
|
150
|
+
// todo - move this to engine
|
|
151
|
+
const dv = new DataView(this.#engineBuffer, ptr);
|
|
152
|
+
const timeCtxPtr = dv.getUint32(TIME_CTX_OFFSET, true);
|
|
153
|
+
const inputCtxPtr = dv.getUint32(INPUT_CTX_OFFSET, true);
|
|
154
|
+
const eventsPtr = dv.getUint32(EVENTS_OFFSET, true);
|
|
155
|
+
|
|
156
|
+
this.#context.rawPointer = ptr;
|
|
157
|
+
this.#context.inputs.dataView = new DataView(
|
|
158
|
+
this.#engineBuffer,
|
|
159
|
+
inputCtxPtr,
|
|
160
|
+
);
|
|
161
|
+
this.#context.time.dataView = new DataView(
|
|
162
|
+
this.#engineBuffer,
|
|
163
|
+
timeCtxPtr,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const eventsDataView = new DataView(this.#engineBuffer, eventsPtr);
|
|
167
|
+
|
|
168
|
+
for (const system of this.#systems) {
|
|
169
|
+
system.update?.(this.#context);
|
|
170
|
+
|
|
171
|
+
const eventCount = eventsDataView.getUint32(0, true);
|
|
172
|
+
|
|
173
|
+
let offset = Uint32Array.BYTES_PER_ELEMENT;
|
|
174
|
+
|
|
175
|
+
for (let i = 0; i < eventCount; i++) {
|
|
176
|
+
const eventType = eventsDataView.getUint8(offset);
|
|
177
|
+
const payloadSize = EVENT_PAYLOAD_SIZE;
|
|
178
|
+
const payloadByte = eventsDataView.getUint8(
|
|
179
|
+
offset + EVENT_PAYLOAD_ALIGN,
|
|
180
|
+
);
|
|
181
|
+
const payloadVec2 = {
|
|
182
|
+
x: eventsDataView.getFloat32(offset + EVENT_PAYLOAD_ALIGN, true),
|
|
183
|
+
y: eventsDataView.getFloat32(
|
|
184
|
+
offset + EVENT_PAYLOAD_ALIGN + Float32Array.BYTES_PER_ELEMENT,
|
|
185
|
+
true,
|
|
186
|
+
),
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
switch (eventType) {
|
|
190
|
+
case Enums.EventType.KeyDown: {
|
|
191
|
+
system.keydown?.(
|
|
192
|
+
attachEvent<KeyEvent, GS>(this.#context, {
|
|
193
|
+
key: keyCodeToKey(payloadByte),
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
case Enums.EventType.KeyUp:
|
|
199
|
+
system.keyup?.(
|
|
200
|
+
attachEvent<KeyEvent, GS>(this.#context, {
|
|
201
|
+
key: keyCodeToKey(payloadByte),
|
|
202
|
+
}),
|
|
203
|
+
);
|
|
204
|
+
break;
|
|
205
|
+
case Enums.EventType.MouseDown:
|
|
206
|
+
system.mousedown?.(
|
|
207
|
+
attachEvent<MouseButtonEvent, GS>(this.#context, {
|
|
208
|
+
button: mouseButtonCodeToMouseButton(payloadByte),
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
break;
|
|
212
|
+
case Enums.EventType.MouseUp:
|
|
213
|
+
system.mouseup?.(
|
|
214
|
+
attachEvent<MouseButtonEvent, GS>(this.#context, {
|
|
215
|
+
button: mouseButtonCodeToMouseButton(payloadByte),
|
|
216
|
+
}),
|
|
217
|
+
);
|
|
218
|
+
break;
|
|
219
|
+
case Enums.EventType.MouseMove:
|
|
220
|
+
system.mousemove?.(
|
|
221
|
+
attachEvent<MouseMoveEvent, GS>(this.#context, {
|
|
222
|
+
x: payloadVec2.x,
|
|
223
|
+
y: payloadVec2.y,
|
|
224
|
+
}),
|
|
225
|
+
);
|
|
226
|
+
break;
|
|
227
|
+
case Enums.EventType.MouseWheel:
|
|
228
|
+
system.mousewheel?.(
|
|
229
|
+
attachEvent<MouseWheelEvent, GS>(this.#context, {
|
|
230
|
+
x: payloadVec2.x,
|
|
231
|
+
y: payloadVec2.y,
|
|
232
|
+
}),
|
|
233
|
+
);
|
|
234
|
+
break;
|
|
235
|
+
default:
|
|
236
|
+
throw new Error(`Unknown event type: ${eventType}`);
|
|
178
237
|
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
attachEvent<KeyEvent, GS>(this.#context, {
|
|
182
|
-
key: keyCodeToKey(payloadByte),
|
|
183
|
-
}),
|
|
184
|
-
);
|
|
185
|
-
break;
|
|
186
|
-
case Enums.EventType.MouseDown:
|
|
187
|
-
system.mousedown?.(
|
|
188
|
-
attachEvent<MouseButtonEvent, GS>(this.#context, {
|
|
189
|
-
button: mouseButtonCodeToMouseButton(payloadByte),
|
|
190
|
-
}),
|
|
191
|
-
);
|
|
192
|
-
break;
|
|
193
|
-
case Enums.EventType.MouseUp:
|
|
194
|
-
system.mouseup?.(
|
|
195
|
-
attachEvent<MouseButtonEvent, GS>(this.#context, {
|
|
196
|
-
button: mouseButtonCodeToMouseButton(payloadByte),
|
|
197
|
-
}),
|
|
198
|
-
);
|
|
199
|
-
break;
|
|
200
|
-
case Enums.EventType.MouseMove:
|
|
201
|
-
system.mousemove?.(
|
|
202
|
-
attachEvent<MouseMoveEvent, GS>(this.#context, {
|
|
203
|
-
x: payloadVec2.x,
|
|
204
|
-
y: payloadVec2.y,
|
|
205
|
-
}),
|
|
206
|
-
);
|
|
207
|
-
break;
|
|
208
|
-
case Enums.EventType.MouseWheel:
|
|
209
|
-
system.mousewheel?.(
|
|
210
|
-
attachEvent<MouseWheelEvent, GS>(this.#context, {
|
|
211
|
-
x: payloadVec2.x,
|
|
212
|
-
y: payloadVec2.y,
|
|
213
|
-
}),
|
|
214
|
-
);
|
|
215
|
-
break;
|
|
216
|
-
default:
|
|
217
|
-
throw new Error(`Unknown event type: ${eventType}`);
|
|
238
|
+
// the event type u8 + padding + payload
|
|
239
|
+
offset += EVENT_PAYLOAD_ALIGN + EVENT_PAYLOAD_SIZE;
|
|
218
240
|
}
|
|
219
|
-
|
|
220
|
-
offset += EVENT_PAYLOAD_ALIGN + EVENT_PAYLOAD_SIZE;
|
|
241
|
+
(this.#context as any).event = undefined;
|
|
221
242
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
setBuffer(buffer: ArrayBuffer) {
|
|
227
|
-
this.#engineBuffer = buffer;
|
|
228
|
-
}
|
|
243
|
+
},
|
|
244
|
+
};
|
|
229
245
|
}
|
|
230
246
|
|
|
231
247
|
function attachEvent<IE extends InputEvent, GS extends BloopSchema>(
|
package/src/mod.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
export * from "./runtime"
|
|
2
|
-
export * from "./
|
|
1
|
+
export type * as typesRuntime from "./runtime"
|
|
2
|
+
export type * as typesMount from "./mount"
|
|
3
|
+
export type * as typesBloop from "./bloop"
|
|
4
|
+
export type * as typesContext from "./context"
|
|
3
5
|
|
|
4
|
-
export * as Util from "./util";
|
|
6
|
+
export * as Util from "./util";
|
|
7
|
+
|
|
8
|
+
export { mount } from "./mount";
|
|
9
|
+
export { Bloop } from "./bloop"
|
package/src/mount.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_WASM_URL,
|
|
3
|
+
type EnginePointer,
|
|
4
|
+
type WasmEngine,
|
|
5
|
+
} from "@bloopjs/engine";
|
|
6
|
+
import { type EngineHooks, Runtime } from "./runtime";
|
|
7
|
+
import { assert } from "./util";
|
|
8
|
+
|
|
9
|
+
export async function mount(opts: MountOpts): Promise<MountResult> {
|
|
10
|
+
// https://github.com/oven-sh/bun/issues/12434
|
|
11
|
+
const bytes = await fetch(opts.wasmUrl ?? DEFAULT_WASM_URL)
|
|
12
|
+
.then((res) => res.arrayBuffer())
|
|
13
|
+
.catch((e) => {
|
|
14
|
+
console.error(
|
|
15
|
+
`Failed to fetch wasm at ${opts.wasmUrl ?? DEFAULT_WASM_URL}`,
|
|
16
|
+
e,
|
|
17
|
+
);
|
|
18
|
+
throw e;
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// 1mb to 64mb
|
|
22
|
+
// use bun check:wasm to find initial memory page size
|
|
23
|
+
const memory = new WebAssembly.Memory({ initial: 17, maximum: 1000 });
|
|
24
|
+
const wasmInstantiatedSource = await WebAssembly.instantiate(bytes, {
|
|
25
|
+
env: {
|
|
26
|
+
memory,
|
|
27
|
+
__cb: function (system_handle: number, ptr: number) {
|
|
28
|
+
opts.hooks.setBuffer(memory.buffer);
|
|
29
|
+
opts.hooks.systemsCallback(system_handle, ptr);
|
|
30
|
+
},
|
|
31
|
+
console_log: function (ptr: EnginePointer, len: number) {
|
|
32
|
+
const bytes = new Uint8Array(memory.buffer, ptr, len);
|
|
33
|
+
const string = new TextDecoder("utf-8").decode(bytes);
|
|
34
|
+
console.log(string);
|
|
35
|
+
},
|
|
36
|
+
user_data_len: function () {
|
|
37
|
+
const serializer = opts.hooks.serialize();
|
|
38
|
+
return serializer ? serializer.size : 0;
|
|
39
|
+
},
|
|
40
|
+
user_data_serialize: function (ptr: EnginePointer, len: number) {
|
|
41
|
+
const serializer = opts.hooks.serialize();
|
|
42
|
+
assert(
|
|
43
|
+
len === serializer.size,
|
|
44
|
+
`user_data_serialize length mismatch, expected=${serializer.size} got=${len}`,
|
|
45
|
+
);
|
|
46
|
+
serializer.write(memory.buffer, ptr);
|
|
47
|
+
},
|
|
48
|
+
user_data_deserialize: function (ptr: EnginePointer, len: number) {
|
|
49
|
+
opts.hooks.deserialize(memory.buffer, ptr, len);
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
const wasm = wasmInstantiatedSource.instance.exports as WasmEngine;
|
|
54
|
+
|
|
55
|
+
wasm.initialize();
|
|
56
|
+
|
|
57
|
+
const runtime = new Runtime(wasm, memory, {
|
|
58
|
+
serialize: opts.hooks.serialize,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (opts.startRecording ?? true) {
|
|
62
|
+
runtime.record();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
runtime,
|
|
67
|
+
wasm,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export type MountOpts = {
|
|
72
|
+
hooks: EngineHooks;
|
|
73
|
+
|
|
74
|
+
wasmUrl?: URL;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether to start recording immediately upon mount
|
|
78
|
+
* Defaults to true
|
|
79
|
+
*/
|
|
80
|
+
startRecording?: boolean;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export type MountResult = {
|
|
84
|
+
runtime: Runtime;
|
|
85
|
+
wasm: WasmEngine;
|
|
86
|
+
};
|
package/src/runtime.ts
CHANGED
|
@@ -6,44 +6,38 @@ import {
|
|
|
6
6
|
type MouseButton,
|
|
7
7
|
mouseButtonToMouseButtonCode,
|
|
8
8
|
SNAPSHOT_HEADER_ENGINE_LEN_OFFSET,
|
|
9
|
-
SNAPSHOT_HEADER_LEN,
|
|
10
9
|
SNAPSHOT_HEADER_USER_LEN_OFFSET,
|
|
11
10
|
TimeContext,
|
|
12
11
|
type WasmEngine,
|
|
13
12
|
} from "@bloopjs/engine";
|
|
14
13
|
import { assert } from "./util";
|
|
15
14
|
|
|
16
|
-
export type
|
|
17
|
-
|
|
15
|
+
export type EngineHooks = {
|
|
16
|
+
/**
|
|
17
|
+
* Hook to serialize some data when snapshotting
|
|
18
|
+
*/
|
|
19
|
+
serialize: SerializeFn;
|
|
20
|
+
/**
|
|
21
|
+
* Hook to deserialize some data when restoring
|
|
22
|
+
*/
|
|
23
|
+
deserialize: DeserializeFn;
|
|
18
24
|
/**
|
|
19
25
|
* A callback function to run logic for a given frame
|
|
20
26
|
*/
|
|
21
|
-
systemsCallback:
|
|
22
|
-
|
|
27
|
+
systemsCallback: SystemsCallback;
|
|
23
28
|
/**
|
|
24
29
|
* Sets buffer to the latest engine memory buffer
|
|
25
30
|
*
|
|
26
31
|
* Note that if the engine wasm memory grows, all dataviews into the memory must be updated
|
|
27
32
|
*/
|
|
28
33
|
setBuffer: (buffer: ArrayBuffer) => void;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Whether to start recording immediately upon mount
|
|
32
|
-
* Defaults to true
|
|
33
|
-
*/
|
|
34
|
-
startRecording?: boolean;
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Optional hook to serialize some data when snapshotting
|
|
38
|
-
*/
|
|
39
|
-
serialize?: SerializeFn;
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Optional hook to deserialize some data when restoring
|
|
43
|
-
*/
|
|
44
|
-
deserialize?: DeserializeFn;
|
|
45
34
|
};
|
|
46
35
|
|
|
36
|
+
export type SystemsCallback = (
|
|
37
|
+
system_handle: number,
|
|
38
|
+
ptr: EnginePointer,
|
|
39
|
+
) => void;
|
|
40
|
+
|
|
47
41
|
export type SerializeFn = () => {
|
|
48
42
|
size: number;
|
|
49
43
|
write(buffer: ArrayBufferLike, ptr: EnginePointer): void;
|
|
@@ -198,76 +192,3 @@ export class Runtime {
|
|
|
198
192
|
},
|
|
199
193
|
};
|
|
200
194
|
}
|
|
201
|
-
|
|
202
|
-
export type MountResult = {
|
|
203
|
-
runtime: Runtime;
|
|
204
|
-
wasm: WasmEngine;
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
export async function mount(opts: MountOpts): Promise<MountResult> {
|
|
208
|
-
if (
|
|
209
|
-
(opts.serialize && !opts.deserialize) ||
|
|
210
|
-
(!opts.serialize && opts.deserialize)
|
|
211
|
-
) {
|
|
212
|
-
throw new Error("Snapshot and restore hooks must be provided together");
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// https://github.com/oven-sh/bun/issues/12434
|
|
216
|
-
const bytes = await Bun.file(opts.wasmUrl ?? DEFAULT_WASM_URL).arrayBuffer();
|
|
217
|
-
|
|
218
|
-
// 1mb to 64mb
|
|
219
|
-
// use bun check:wasm to find initial memory page size
|
|
220
|
-
const memory = new WebAssembly.Memory({ initial: 17, maximum: 1000 });
|
|
221
|
-
const wasmInstantiatedSource = await WebAssembly.instantiate(bytes, {
|
|
222
|
-
env: {
|
|
223
|
-
memory,
|
|
224
|
-
__cb: function (system_handle: number, ptr: number) {
|
|
225
|
-
opts.setBuffer(memory.buffer);
|
|
226
|
-
opts.systemsCallback(system_handle, ptr);
|
|
227
|
-
},
|
|
228
|
-
console_log: function (ptr: EnginePointer, len: number) {
|
|
229
|
-
const bytes = new Uint8Array(memory.buffer, ptr, len);
|
|
230
|
-
const string = new TextDecoder("utf-8").decode(bytes);
|
|
231
|
-
console.log(string);
|
|
232
|
-
},
|
|
233
|
-
user_data_len: function () {
|
|
234
|
-
const serializer = opts.serialize ? opts.serialize() : null;
|
|
235
|
-
return serializer ? serializer.size : 0;
|
|
236
|
-
},
|
|
237
|
-
user_data_serialize: function (ptr: EnginePointer, len: number) {
|
|
238
|
-
if (!opts.serialize) {
|
|
239
|
-
return;
|
|
240
|
-
}
|
|
241
|
-
const serializer = opts.serialize();
|
|
242
|
-
if (len !== serializer.size) {
|
|
243
|
-
throw new Error(
|
|
244
|
-
`user_data_write length mismatch, expected=${serializer.size} got=${len}`,
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
serializer.write(memory.buffer, ptr);
|
|
248
|
-
},
|
|
249
|
-
user_data_deserialize: function (ptr: EnginePointer, len: number) {
|
|
250
|
-
if (!opts.deserialize) {
|
|
251
|
-
return;
|
|
252
|
-
}
|
|
253
|
-
opts.deserialize(memory.buffer, ptr, len);
|
|
254
|
-
},
|
|
255
|
-
},
|
|
256
|
-
});
|
|
257
|
-
const wasm = wasmInstantiatedSource.instance.exports as WasmEngine;
|
|
258
|
-
|
|
259
|
-
wasm.initialize();
|
|
260
|
-
|
|
261
|
-
const runtime = new Runtime(wasm, memory, {
|
|
262
|
-
serialize: opts.serialize,
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
if (opts.startRecording ?? true) {
|
|
266
|
-
runtime.record();
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
return {
|
|
270
|
-
runtime,
|
|
271
|
-
wasm,
|
|
272
|
-
};
|
|
273
|
-
}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: Use Bun instead of Node.js, npm, pnpm, or vite.
|
|
3
|
-
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
|
|
4
|
-
alwaysApply: false
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
Default to using Bun instead of Node.js.
|
|
8
|
-
|
|
9
|
-
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
|
10
|
-
- Use `bun test` instead of `jest` or `vitest`
|
|
11
|
-
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
|
12
|
-
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
|
13
|
-
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
|
14
|
-
- Bun automatically loads .env, so don't use dotenv.
|
|
15
|
-
|
|
16
|
-
## APIs
|
|
17
|
-
|
|
18
|
-
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
|
19
|
-
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
|
20
|
-
- `Bun.redis` for Redis. Don't use `ioredis`.
|
|
21
|
-
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
|
22
|
-
- `WebSocket` is built-in. Don't use `ws`.
|
|
23
|
-
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
|
24
|
-
- Bun.$`ls` instead of execa.
|
|
25
|
-
|
|
26
|
-
## Testing
|
|
27
|
-
|
|
28
|
-
Use `bun test` to run tests.
|
|
29
|
-
|
|
30
|
-
```ts#index.test.ts
|
|
31
|
-
import { test, expect } from "bun:test";
|
|
32
|
-
|
|
33
|
-
test("hello world", () => {
|
|
34
|
-
expect(1).toBe(1);
|
|
35
|
-
});
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## Frontend
|
|
39
|
-
|
|
40
|
-
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
|
41
|
-
|
|
42
|
-
Server:
|
|
43
|
-
|
|
44
|
-
```ts#index.ts
|
|
45
|
-
import index from "./index.html"
|
|
46
|
-
|
|
47
|
-
Bun.serve({
|
|
48
|
-
routes: {
|
|
49
|
-
"/": index,
|
|
50
|
-
"/api/users/:id": {
|
|
51
|
-
GET: (req) => {
|
|
52
|
-
return new Response(JSON.stringify({ id: req.params.id }));
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
// optional websocket support
|
|
57
|
-
websocket: {
|
|
58
|
-
open: (ws) => {
|
|
59
|
-
ws.send("Hello, world!");
|
|
60
|
-
},
|
|
61
|
-
message: (ws, message) => {
|
|
62
|
-
ws.send(message);
|
|
63
|
-
},
|
|
64
|
-
close: (ws) => {
|
|
65
|
-
// handle close
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
development: {
|
|
69
|
-
hmr: true,
|
|
70
|
-
console: true,
|
|
71
|
-
}
|
|
72
|
-
})
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
|
76
|
-
|
|
77
|
-
```html#index.html
|
|
78
|
-
<html>
|
|
79
|
-
<body>
|
|
80
|
-
<h1>Hello, world!</h1>
|
|
81
|
-
<script type="module" src="./frontend.tsx"></script>
|
|
82
|
-
</body>
|
|
83
|
-
</html>
|
|
84
|
-
```
|
|
85
|
-
|
|
86
|
-
With the following `frontend.tsx`:
|
|
87
|
-
|
|
88
|
-
```tsx#frontend.tsx
|
|
89
|
-
import React from "react";
|
|
90
|
-
|
|
91
|
-
// import .css files directly and it works
|
|
92
|
-
import './index.css';
|
|
93
|
-
|
|
94
|
-
import { createRoot } from "react-dom/client";
|
|
95
|
-
|
|
96
|
-
const root = createRoot(document.body);
|
|
97
|
-
|
|
98
|
-
export default function Frontend() {
|
|
99
|
-
return <h1>Hello, world!</h1>;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
root.render(<Frontend />);
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
Then, run index.ts
|
|
106
|
-
|
|
107
|
-
```sh
|
|
108
|
-
bun --hot ./index.ts
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.
|
package/jsr.json
DELETED