aboutsummaryrefslogtreecommitdiff
path: root/src/runtime
diff options
context:
space:
mode:
authorGravatar Jarred Sumner <jarred@jarredsumner.com> 2021-06-12 19:10:08 -0700
committerGravatar Jarred Sumner <jarred@jarredsumner.com> 2021-06-12 19:10:08 -0700
commitc51c65325faf6692d3eebf92927f56cf35f6b613 (patch)
tree892254ba9a7f0241944283d6766ab54ca0c35e8a /src/runtime
parentf43234bc300b7e9d9d572dc1b7d8e156ad01576a (diff)
downloadbun-c51c65325faf6692d3eebf92927f56cf35f6b613.tar.gz
bun-c51c65325faf6692d3eebf92927f56cf35f6b613.tar.zst
bun-c51c65325faf6692d3eebf92927f56cf35f6b613.zip
I think thats the JS part of HMR
Former-commit-id: 43380a4d68d57f3d78f5b1e00962a59461140967
Diffstat (limited to '')
-rw-r--r--src/runtime.zig7
-rw-r--r--src/runtime/hmr.ts391
-rw-r--r--src/runtime/index.ts2
3 files changed, 397 insertions, 3 deletions
diff --git a/src/runtime.zig b/src/runtime.zig
index f74ed8067..f6a15da98 100644
--- a/src/runtime.zig
+++ b/src/runtime.zig
@@ -2,12 +2,12 @@ const options = @import("./options.zig");
usingnamespace @import("ast/base.zig");
usingnamespace @import("global.zig");
const std = @import("std");
-pub const ProdSourceContent = @embedFile("./runtime.js");
+pub const ProdSourceContent = @embedFile("./runtime.out.js");
pub const Runtime = struct {
pub fn sourceContent() string {
if (isDebug) {
- var runtime_path = std.fs.path.join(std.heap.c_allocator, &[_]string{ std.fs.path.dirname(@src().file).?, "runtime.js" }) catch unreachable;
+ var runtime_path = std.fs.path.join(std.heap.c_allocator, &[_]string{ std.fs.path.dirname(@src().file).?, "runtime.out.js" }) catch unreachable;
const file = std.fs.openFileAbsolute(runtime_path, .{}) catch unreachable;
defer file.close();
return file.readToEndAlloc(std.heap.c_allocator, (file.stat() catch unreachable).size) catch unreachable;
@@ -19,7 +19,8 @@ pub const Runtime = struct {
pub fn version() string {
return version_hash;
}
- pub const Features = packed struct {
+
+ pub const Features = struct {
react_fast_refresh: bool = false,
hot_module_reloading: bool = false,
keep_names_for_arrow_functions: bool = true,
diff --git a/src/runtime/hmr.ts b/src/runtime/hmr.ts
new file mode 100644
index 000000000..f6d540b5a
--- /dev/null
+++ b/src/runtime/hmr.ts
@@ -0,0 +1,391 @@
+import { ByteBuffer } from "peechy/bb";
+import * as Schema from "../api/schema";
+
+var runOnce = false;
+var clientStartTime = 0;
+
+function formatDuration(duration: number) {
+ return Math.round(duration * 100000) / 100;
+}
+
+export class Client {
+ socket: WebSocket;
+ hasWelcomed: boolean = false;
+ reconnect: number = 0;
+ // Server timestamps are relative to the time the server's HTTP server launched
+ // This so we can send timestamps as uint32 instead of 128-bit integers
+ epoch: number = 0;
+
+ start() {
+ if (runOnce) {
+ console.warn(
+ "[speedy] Attempted to start HMR client multiple times. This may be a bug."
+ );
+ return;
+ }
+
+ runOnce = true;
+ this.connect();
+ }
+
+ connect() {
+ clientStartTime = performance.now();
+
+ this.socket = new WebSocket("/_api", ["speedy-hmr"]);
+ this.socket.binaryType = "arraybuffer";
+ this.socket.onclose = this.handleClose;
+ this.socket.onopen = this.handleOpen;
+ this.socket.onmessage = this.handleMessage;
+ }
+
+ // key: module id
+ // value: server-timestamp
+ builds = new Map<number, number>();
+
+ indexOfModuleId(id: number): number {
+ return Module.dependencies.graph.indexOf(id);
+ }
+
+ handleBuildFailure(buffer: ByteBuffer, timestamp: number) {
+ // 0: ID
+ // 1: Timestamp
+ const header_data = new Uint32Array(
+ buffer._data.buffer,
+ buffer._data.byteOffset,
+ buffer._data.byteOffset + 8
+ );
+ const index = this.indexOfModuleId(header_data[0]);
+ // Ignore build failures of modules that are not loaded
+ if (index === -1) {
+ return;
+ }
+
+ // Build failed for a module we didn't request?
+ const minTimestamp = this.builds.get(index);
+ if (!minTimestamp) {
+ return;
+ }
+ const fail = Schema.decodeWebsocketMessageBuildFailure(buffer);
+ // TODO: finish this.
+ console.error("[speedy] Build failed", fail.module_path);
+ }
+
+ verbose = process.env.SPEEDY_HMR_VERBOSE;
+
+ handleBuildSuccess(buffer: ByteBuffer, timestamp: number) {
+ // 0: ID
+ // 1: Timestamp
+ const header_data = new Uint32Array(
+ buffer._data.buffer,
+ buffer._data.byteOffset,
+ buffer._data.byteOffset + 8
+ );
+ const index = this.indexOfModuleId(header_data[0]);
+ // Ignore builds of modules that are not loaded
+ if (index === -1) {
+ if (this.verbose) {
+ console.debug(
+ `[speedy] Skipping reload for unknown module id:`,
+ header_data[0]
+ );
+ }
+
+ return;
+ }
+
+ // Ignore builds of modules we expect a later version of
+ const currentVersion = this.builds.get(header_data[0]) || -Infinity;
+ if (currentVersion > header_data[1]) {
+ if (this.verbose) {
+ console.debug(
+ `[speedy] Ignoring module update for "${Module.dependencies.modules[index].url.pathname}" due to timestamp mismatch.\n Expected: >=`,
+ currentVersion,
+ `\n Received:`,
+ header_data[1]
+ );
+ }
+ return;
+ }
+
+ if (this.verbose) {
+ console.debug(
+ "[speedy] Preparing to reload",
+ Module.dependencies.modules[index].url.pathname
+ );
+ }
+
+ const build = Schema.decodeWebsocketMessageBuildSuccess(buffer);
+ var reload = new HotReload(header_data[0], index, build);
+ reload.timings.notify = timestamp - build.from_timestamp;
+ reload.run().then(
+ ([module, timings]) => {
+ console.log(
+ `[speedy] Reloaded in ${formatDuration(timings.total)}ms :`,
+ module.url.pathname
+ );
+ },
+ (err) => {
+ console.error("[speedy] Hot Module Reload failed!", err);
+ debugger;
+ }
+ );
+ }
+
+ handleFileChangeNotification(buffer: ByteBuffer, timestamp: number) {
+ const notification =
+ Schema.decodeWebsocketMessageFileChangeNotification(buffer);
+ const index = Module.dependencies.graph.indexOf(notification.id);
+
+ if (index === -1) {
+ if (this.verbose) {
+ console.debug("[speedy] Unknown module changed, skipping");
+ }
+ return;
+ }
+
+ if ((this.builds.get(notification.id) || -Infinity) > timestamp) {
+ console.debug(
+ `[speedy] Received update for ${Module.dependencies.modules[index].url.pathname}`
+ );
+ return;
+ }
+
+ if (this.verbose) {
+ console.debug(
+ `[speedy] Requesting update for ${Module.dependencies.modules[index].url.pathname}`
+ );
+ }
+
+ this.builds.set(notification.id, timestamp);
+ this.buildCommandBuf[0] = Schema.WebsocketCommandKind.build;
+ this.buildCommandUArray[0] = timestamp;
+ this.buildCommandBuf.set(new Uint8Array(this.buildCommandUArray), 1);
+ this.buildCommandUArray[0] = notification.id;
+ this.buildCommandBuf.set(new Uint8Array(this.buildCommandUArray), 5);
+ this.socket.send(this.buildCommandBuf);
+ }
+ buildCommandBuf = new Uint8Array(9);
+ buildCommandUArray = new Uint32Array(1);
+
+ handleOpen = (event: Event) => {
+ globalThis.clearInterval(this.reconnect);
+ this.reconnect = 0;
+ };
+
+ handleMessage = (event: MessageEvent) => {
+ const data = new Uint8Array(event.data);
+ const message_header_byte_buffer = new ByteBuffer(data);
+ const header = Schema.decodeWebsocketMessage(message_header_byte_buffer);
+ const buffer = new ByteBuffer(
+ data.subarray(message_header_byte_buffer._index)
+ );
+
+ switch (header.kind) {
+ case Schema.WebsocketMessageKind.build_fail: {
+ this.handleBuildFailure(buffer, header.timestamp);
+ break;
+ }
+ case Schema.WebsocketMessageKind.build_success: {
+ this.handleBuildSuccess(buffer, header.timestamp);
+ break;
+ }
+ case Schema.WebsocketMessageKind.file_change_notification: {
+ this.handleFileChangeNotification(buffer, header.timestamp);
+ break;
+ }
+ case Schema.WebsocketMessageKind.welcome: {
+ const now = performance.now();
+ console.log(
+ "[speedy] HMR connected in",
+ formatDuration(now - clientStartTime),
+ "ms"
+ );
+ clientStartTime = now;
+ this.hasWelcomed = true;
+ const welcome = Schema.decodeWebsocketMessageWelcome(buffer);
+ this.epoch = welcome.epoch;
+ if (!this.epoch) {
+ console.warn("[speedy] Internal HMR error");
+ }
+ break;
+ }
+ }
+ };
+
+ handleClose = (event: CloseEvent) => {
+ if (this.reconnect !== 0) {
+ return;
+ }
+
+ this.reconnect = setInterval(this.connect, 500) as any as number;
+ console.warn("[speedy] HMR disconnected. Attempting to reconnect.");
+ };
+}
+
+class HotReload {
+ module_id: number = 0;
+ module_index: number = 0;
+ build: Schema.WebsocketMessageBuildSuccess;
+ timings = {
+ notify: 0,
+ decode: 0,
+ import: 0,
+ callbacks: 0,
+ total: 0,
+ start: 0,
+ };
+
+ constructor(
+ module_id: HotReload["module_id"],
+ module_index: HotReload["module_index"],
+ build: HotReload["build"]
+ ) {
+ this.module_id = module_id;
+ this.module_index = module_index;
+ this.build = build;
+ }
+
+ async run(): Promise<[Module, HotReload["timings"]]> {
+ const importStart = performance.now();
+ let orig_deps = Module.dependencies;
+ Module.dependencies = orig_deps.fork(this.module_index);
+ var blobURL = null;
+ try {
+ const blob = new Blob([this.build.bytes], { type: "text/javascript" });
+ blobURL = URL.createObjectURL(blob);
+ await import(blobURL);
+ this.timings.import = performance.now() - importStart;
+ } catch (exception) {
+ Module.dependencies = orig_deps;
+ URL.revokeObjectURL(blobURL);
+ throw exception;
+ }
+
+ URL.revokeObjectURL(blobURL);
+
+ if (process.env.SPEEDY_HMR_VERBOSE) {
+ console.debug(
+ "[speedy] Re-imported",
+ Module.dependencies.modules[this.module_index].url.pathname,
+ "in",
+ formatDuration(this.timings.import),
+ ". Running callbacks"
+ );
+ }
+
+ const callbacksStart = performance.now();
+ try {
+ // ES Modules delay execution until all imports are parsed
+ // They execute depth-first
+ // If you load N modules and append each module ID to the array, 0 is the *last* module imported.
+ // modules.length - 1 is the first.
+ // Therefore, to reload all the modules in the correct order, we traverse the graph backwards
+ // This only works when the graph is up to date.
+ // If the import order changes, we need to regenerate the entire graph
+ // Which sounds expensive, until you realize that we are mostly talking about an array that will be typically less than 1024 elements
+ // Computers can do that in < 1ms easy!
+ for (let i = Module.dependencies.graph_used; i > this.module_index; i--) {
+ let handled = !Module.dependencies.modules[i].exports.__hmrDisable;
+ if (typeof Module.dependencies.modules[i].dispose === "function") {
+ Module.dependencies.modules[i].dispose();
+ handled = true;
+ }
+ if (typeof Module.dependencies.modules[i].accept === "function") {
+ Module.dependencies.modules[i].accept();
+ handled = true;
+ }
+ if (!handled) {
+ Module.dependencies.modules[i]._load();
+ }
+ }
+ } catch (exception) {
+ Module.dependencies = orig_deps;
+ throw exception;
+ }
+ this.timings.callbacks = performance.now() - callbacksStart;
+
+ if (process.env.SPEEDY_HMR_VERBOSE) {
+ console.debug(
+ "[speedy] Ran callbacks",
+ Module.dependencies.modules[this.module_index].url.pathname,
+ "in",
+ formatDuration(this.timings.callbacks),
+ "ms"
+ );
+ }
+
+ orig_deps = null;
+ this.timings.total =
+ this.timings.import + this.timings.callbacks + this.build.from_timestamp;
+ return Promise.resolve([
+ Module.dependencies.modules[this.module_index],
+ this.timings,
+ ]);
+ }
+}
+var client: Client;
+if ("SPEEDY_HMR_CLIENT" in globalThis) {
+ console.warn(
+ "[speedy] Attempted to load multiple copies of HMR. This may be a bug."
+ );
+} else if (process.env.SPEEDY_HMR_ENABLED) {
+ client = new Client();
+ client.start();
+ globalThis.SPEEDY_HMR_CLIENT = client;
+}
+
+export class Module {
+ constructor(id: number, url: URL) {
+ // Ensure V8 knows this is a U32
+ this.id = id | 0;
+ this.url = url;
+
+ if (!Module._dependencies) {
+ Module.dependencies = Module._dependencies;
+ }
+
+ this.graph_index = Module.dependencies.graph_used++;
+
+ // Grow the dependencies graph
+ if (Module.dependencies.graph.length <= this.graph_index) {
+ const new_graph = new Uint32Array(Module.dependencies.graph.length * 4);
+ new_graph.set(Module.dependencies.graph);
+ Module.dependencies.graph = new_graph;
+
+ // In-place grow. This creates a holey array, which is bad, but less bad than pushing potentially 1000 times
+ Module.dependencies.modules.length = new_graph.length;
+ }
+
+ Module.dependencies.modules[this.graph_index] = this;
+ Module.dependencies.graph[this.graph_index] = this.id | 0;
+ }
+ additional_files = [];
+
+ // When a module updates, we need to re-initialize each dependent, recursively
+ // To do so:
+ // 1. Track which modules are imported by which *at runtime*
+ // 2. When A updates, loop through each dependent of A in insertion order
+ // 3. For each old dependent, call .dispose() if exists
+ // 3. For each new dependent, call .accept() if exists
+ // 4.
+ static _dependencies = {
+ modules: new Array<Module>(32),
+ graph: new Uint32Array(32),
+ graph_used: 0,
+
+ fork(offset: number) {
+ return {
+ modules: Module._dependencies.modules.slice(),
+ graph: Module._dependencies.graph.slice(),
+ graph_used: offset - 1,
+ };
+ },
+ };
+ static dependencies: Module["_dependencies"];
+ url: URL;
+ _load = function () {};
+ id = 0;
+ graph_index = 0;
+ _exports = {};
+ exports = {};
+}
diff --git a/src/runtime/index.ts b/src/runtime/index.ts
new file mode 100644
index 000000000..873666412
--- /dev/null
+++ b/src/runtime/index.ts
@@ -0,0 +1,2 @@
+export * from "./hmr";
+export * from "../runtime.js";