diff options
-rw-r--r-- | src/analytics/analytics_thread.zig | 15 | ||||
-rw-r--r-- | src/api/schema.d.ts | 78 | ||||
-rw-r--r-- | src/api/schema.js | 196 | ||||
-rw-r--r-- | src/api/schema.peechy | 38 | ||||
-rw-r--r-- | src/api/schema.zig | 133 | ||||
-rw-r--r-- | src/http.zig | 261 | ||||
-rw-r--r-- | src/http/websocket.zig | 2 | ||||
-rw-r--r-- | src/http_client_async.zig | 37 | ||||
-rw-r--r-- | src/runtime/hmr.ts | 124 | ||||
-rw-r--r-- | src/watcher.zig | 70 |
10 files changed, 472 insertions, 482 deletions
diff --git a/src/analytics/analytics_thread.zig b/src/analytics/analytics_thread.zig index c77ae4075..02ae90893 100644 --- a/src/analytics/analytics_thread.zig +++ b/src/analytics/analytics_thread.zig @@ -321,7 +321,7 @@ var counter: std.atomic.Atomic(u32) = undefined; fn start() bool { @setCold(true); - defer has_loaded = true; + has_loaded = true; counter = std.atomic.Atomic(u32).init(0); event_queue = EventQueue.init(std.heap.c_allocator); @@ -352,23 +352,26 @@ const header_entry = Headers.Kv{ }, }; +var out_buffer: MutableString = undefined; +var event_list: EventList = undefined; fn readloop() anyerror!void { defer disabled = true; Output.Source.configureNamedThread(thread, "Analytics"); defer Output.flush(); - var event_list = try default_allocator.create(EventList); - event_list.* = EventList.init(); + + event_list = EventList.init(); var headers_entries: Headers.Entries = Headers.Entries{}; headers_entries.append(default_allocator, header_entry) catch unreachable; + out_buffer = try MutableString.init(default_allocator, 64); event_list.async_http = HTTP.AsyncHTTP.init( default_allocator, .POST, URL.parse(Environment.analytics_url), headers_entries, headers_buf, - &event_list.out_buffer, + &out_buffer, &event_list.in_buffer, std.time.ns_per_ms * 10000, ) catch return; @@ -395,7 +398,6 @@ pub const EventList = struct { events: std.ArrayList(Event), async_http: HTTP.AsyncHTTP, - out_buffer: MutableString, in_buffer: MutableString, pub fn init() EventList { @@ -405,7 +407,6 @@ pub const EventList = struct { .events = std.ArrayList(Event).init(default_allocator), .in_buffer = MutableString.init(default_allocator, 1024) catch unreachable, .async_http = undefined, - .out_buffer = MutableString.init(default_allocator, 0) catch unreachable, }; } @@ -510,7 +511,7 @@ pub const EventList = struct { disabled = disabled or stuck_count > 4; this.in_buffer.reset(); - this.out_buffer.reset(); +out_buffer.reset(); if (comptime FeatureFlags.verbose_analytics) { Output.prettyErrorln("[Analytics] Sent {d} events", .{count}); diff --git a/src/api/schema.d.ts b/src/api/schema.d.ts index d00dcfa6e..273f8d9d3 100644 --- a/src/api/schema.d.ts +++ b/src/api/schema.d.ts @@ -231,6 +231,8 @@ export enum WebsocketMessageKind { build_fail = 4, manifest_success = 5, manifest_fail = 6, + resolve_file = 7, + file_change_notification_with_hint = 8, } export const WebsocketMessageKindKeys = { 1: "welcome", @@ -245,16 +247,23 @@ export const WebsocketMessageKindKeys = { manifest_success: "manifest_success", 6: "manifest_fail", manifest_fail: "manifest_fail", + 7: "resolve_file", + resolve_file: "resolve_file", + 8: "file_change_notification_with_hint", + file_change_notification_with_hint: "file_change_notification_with_hint", }; export enum WebsocketCommandKind { build = 1, manifest = 2, + build_with_file_path = 3, } export const WebsocketCommandKindKeys = { 1: "build", build: "build", 2: "manifest", manifest: "manifest", + 3: "build_with_file_path", + build_with_file_path: "build_with_file_path", }; export interface StackFrame { function_name: string; @@ -575,37 +584,13 @@ export interface WebsocketMessageBuildFailure { log: Log; } -export interface DependencyManifest { - ids: Uint32Array; -} - -export interface FileList { - ptrs: StringPointer[]; - files: string; -} - -export interface WebsocketMessageResolveIDs { - id: Uint32Array; - list: FileList; -} - -export interface WebsocketCommandResolveIDs { - ptrs: StringPointer[]; - files: string; -} - -export interface WebsocketMessageManifestSuccess { +export interface WebsocketCommandBuildWithFilePath { id: uint32; - module_path: string; - loader: Loader; - manifest: DependencyManifest; + file_path: string; } -export interface WebsocketMessageManifestFailure { +export interface WebsocketMessageResolveID { id: uint32; - from_timestamp: uint32; - loader: Loader; - log: Log; } export declare function encodeStackFrame( @@ -860,40 +845,17 @@ export declare function encodeWebsocketMessageBuildFailure( export declare function decodeWebsocketMessageBuildFailure( buffer: ByteBuffer ): WebsocketMessageBuildFailure; -export declare function encodeDependencyManifest( - message: DependencyManifest, - bb: ByteBuffer -): void; -export declare function decodeDependencyManifest( - buffer: ByteBuffer -): DependencyManifest; -export declare function encodeFileList(message: FileList, bb: ByteBuffer): void; -export declare function decodeFileList(buffer: ByteBuffer): FileList; -export declare function encodeWebsocketMessageResolveIDs( - message: WebsocketMessageResolveIDs, - bb: ByteBuffer -): void; -export declare function decodeWebsocketMessageResolveIDs( - buffer: ByteBuffer -): WebsocketMessageResolveIDs; -export declare function encodeWebsocketCommandResolveIDs( - message: WebsocketCommandResolveIDs, - bb: ByteBuffer -): void; -export declare function decodeWebsocketCommandResolveIDs( - buffer: ByteBuffer -): WebsocketCommandResolveIDs; -export declare function encodeWebsocketMessageManifestSuccess( - message: WebsocketMessageManifestSuccess, +export declare function encodeWebsocketCommandBuildWithFilePath( + message: WebsocketCommandBuildWithFilePath, bb: ByteBuffer ): void; -export declare function decodeWebsocketMessageManifestSuccess( +export declare function decodeWebsocketCommandBuildWithFilePath( buffer: ByteBuffer -): WebsocketMessageManifestSuccess; -export declare function encodeWebsocketMessageManifestFailure( - message: WebsocketMessageManifestFailure, +): WebsocketCommandBuildWithFilePath; +export declare function encodeWebsocketMessageResolveID( + message: WebsocketMessageResolveID, bb: ByteBuffer ): void; -export declare function decodeWebsocketMessageManifestFailure( +export declare function decodeWebsocketMessageResolveID( buffer: ByteBuffer -): WebsocketMessageManifestFailure; +): WebsocketMessageResolveID; diff --git a/src/api/schema.js b/src/api/schema.js index dc89e2b11..8b2043e9c 100644 --- a/src/api/schema.js +++ b/src/api/schema.js @@ -2369,12 +2369,16 @@ const WebsocketMessageKind = { 4: 4, 5: 5, 6: 6, + 7: 7, + 8: 8, welcome: 1, file_change_notification: 2, build_success: 3, build_fail: 4, manifest_success: 5, manifest_fail: 6, + resolve_file: 7, + file_change_notification_with_hint: 8, }; const WebsocketMessageKindKeys = { 1: "welcome", @@ -2383,24 +2387,32 @@ const WebsocketMessageKindKeys = { 4: "build_fail", 5: "manifest_success", 6: "manifest_fail", + 7: "resolve_file", + 8: "file_change_notification_with_hint", welcome: "welcome", file_change_notification: "file_change_notification", build_success: "build_success", build_fail: "build_fail", manifest_success: "manifest_success", manifest_fail: "manifest_fail", + resolve_file: "resolve_file", + file_change_notification_with_hint: "file_change_notification_with_hint", }; const WebsocketCommandKind = { 1: 1, 2: 2, + 3: 3, build: 1, manifest: 2, + build_with_file_path: 3, }; const WebsocketCommandKindKeys = { 1: "build", 2: "manifest", + 3: "build_with_file_path", build: "build", manifest: "manifest", + build_with_file_path: "build_with_file_path", }; function decodeWebsocketMessage(bb) { @@ -2669,121 +2681,15 @@ function encodeWebsocketMessageBuildFailure(message, bb) { } } -function decodeDependencyManifest(bb) { - var result = {}; - - result["ids"] = bb.readUint32ByteArray(); - return result; -} - -function encodeDependencyManifest(message, bb) { - var value = message["ids"]; - if (value != null) { - bb.writeUint32ByteArray(value); - } else { - throw new Error('Missing required field "ids"'); - } -} - -function decodeFileList(bb) { - var result = {}; - - var length = bb.readVarUint(); - var values = (result["ptrs"] = Array(length)); - for (var i = 0; i < length; i++) values[i] = decodeStringPointer(bb); - result["files"] = bb.readString(); - return result; -} - -function encodeFileList(message, bb) { - var value = message["ptrs"]; - if (value != null) { - var values = value, - n = values.length; - bb.writeVarUint(n); - for (var i = 0; i < n; i++) { - value = values[i]; - encodeStringPointer(value, bb); - } - } else { - throw new Error('Missing required field "ptrs"'); - } - - var value = message["files"]; - if (value != null) { - bb.writeString(value); - } else { - throw new Error('Missing required field "files"'); - } -} - -function decodeWebsocketMessageResolveIDs(bb) { - var result = {}; - - result["id"] = bb.readUint32ByteArray(); - result["list"] = decodeFileList(bb); - return result; -} - -function encodeWebsocketMessageResolveIDs(message, bb) { - var value = message["id"]; - if (value != null) { - bb.writeUint32ByteArray(value); - } else { - throw new Error('Missing required field "id"'); - } - - var value = message["list"]; - if (value != null) { - encodeFileList(value, bb); - } else { - throw new Error('Missing required field "list"'); - } -} - -function decodeWebsocketCommandResolveIDs(bb) { - var result = {}; - - var length = bb.readVarUint(); - var values = (result["ptrs"] = Array(length)); - for (var i = 0; i < length; i++) values[i] = decodeStringPointer(bb); - result["files"] = bb.readString(); - return result; -} - -function encodeWebsocketCommandResolveIDs(message, bb) { - var value = message["ptrs"]; - if (value != null) { - var values = value, - n = values.length; - bb.writeVarUint(n); - for (var i = 0; i < n; i++) { - value = values[i]; - encodeStringPointer(value, bb); - } - } else { - throw new Error('Missing required field "ptrs"'); - } - - var value = message["files"]; - if (value != null) { - bb.writeString(value); - } else { - throw new Error('Missing required field "files"'); - } -} - -function decodeWebsocketMessageManifestSuccess(bb) { +function decodeWebsocketCommandBuildWithFilePath(bb) { var result = {}; result["id"] = bb.readUint32(); - result["module_path"] = bb.readString(); - result["loader"] = Loader[bb.readByte()]; - result["manifest"] = decodeDependencyManifest(bb); + result["file_path"] = bb.readString(); return result; } -function encodeWebsocketMessageManifestSuccess(message, bb) { +function encodeWebsocketCommandBuildWithFilePath(message, bb) { var value = message["id"]; if (value != null) { bb.writeUint32(value); @@ -2791,76 +2697,28 @@ function encodeWebsocketMessageManifestSuccess(message, bb) { throw new Error('Missing required field "id"'); } - var value = message["module_path"]; + var value = message["file_path"]; if (value != null) { bb.writeString(value); } else { - throw new Error('Missing required field "module_path"'); - } - - var value = message["loader"]; - if (value != null) { - var encoded = Loader[value]; - if (encoded === void 0) - throw new Error( - "Invalid value " + JSON.stringify(value) + ' for enum "Loader"' - ); - bb.writeByte(encoded); - } else { - throw new Error('Missing required field "loader"'); - } - - var value = message["manifest"]; - if (value != null) { - encodeDependencyManifest(value, bb); - } else { - throw new Error('Missing required field "manifest"'); + throw new Error('Missing required field "file_path"'); } } -function decodeWebsocketMessageManifestFailure(bb) { +function decodeWebsocketMessageResolveID(bb) { var result = {}; result["id"] = bb.readUint32(); - result["from_timestamp"] = bb.readUint32(); - result["loader"] = Loader[bb.readByte()]; - result["log"] = decodeLog(bb); return result; } -function encodeWebsocketMessageManifestFailure(message, bb) { +function encodeWebsocketMessageResolveID(message, bb) { var value = message["id"]; if (value != null) { bb.writeUint32(value); } else { throw new Error('Missing required field "id"'); } - - var value = message["from_timestamp"]; - if (value != null) { - bb.writeUint32(value); - } else { - throw new Error('Missing required field "from_timestamp"'); - } - - var value = message["loader"]; - if (value != null) { - var encoded = Loader[value]; - if (encoded === void 0) - throw new Error( - "Invalid value " + JSON.stringify(value) + ' for enum "Loader"' - ); - bb.writeByte(encoded); - } else { - throw new Error('Missing required field "loader"'); - } - - var value = message["log"]; - if (value != null) { - encodeLog(value, bb); - } else { - throw new Error('Missing required field "log"'); - } } export { Loader }; @@ -2985,15 +2843,7 @@ export { decodeWebsocketMessageBuildSuccess }; export { encodeWebsocketMessageBuildSuccess }; export { decodeWebsocketMessageBuildFailure }; export { encodeWebsocketMessageBuildFailure }; -export { decodeDependencyManifest }; -export { encodeDependencyManifest }; -export { decodeFileList }; -export { encodeFileList }; -export { decodeWebsocketMessageResolveIDs }; -export { encodeWebsocketMessageResolveIDs }; -export { decodeWebsocketCommandResolveIDs }; -export { encodeWebsocketCommandResolveIDs }; -export { decodeWebsocketMessageManifestSuccess }; -export { encodeWebsocketMessageManifestSuccess }; -export { decodeWebsocketMessageManifestFailure }; -export { encodeWebsocketMessageManifestFailure }; +export { decodeWebsocketCommandBuildWithFilePath }; +export { encodeWebsocketCommandBuildWithFilePath }; +export { decodeWebsocketMessageResolveID }; +export { encodeWebsocketMessageResolveID }; diff --git a/src/api/schema.peechy b/src/api/schema.peechy index 8afb0f30a..7c5b482a2 100644 --- a/src/api/schema.peechy +++ b/src/api/schema.peechy @@ -440,11 +440,14 @@ smol WebsocketMessageKind { build_fail = 4; manifest_success = 5; manifest_fail = 6; + resolve_file = 7; + file_change_notification_with_hint = 8; } smol WebsocketCommandKind { build = 1; manifest = 2; + build_with_file_path = 3; } // Each websocket message has two messages in it! @@ -492,7 +495,6 @@ struct WebsocketMessageBuildSuccess { uint32 blob_length; } - struct WebsocketMessageBuildFailure { uint32 id; uint32 from_timestamp; @@ -502,37 +504,11 @@ struct WebsocketMessageBuildFailure { Log log; } -// CSS @import only for now! -struct DependencyManifest { - uint32[] ids; -} - -struct FileList { - StringPointer[] ptrs; - string files; -} - -struct WebsocketMessageResolveIDs { - uint32[] id; - FileList list; -} - -struct WebsocketCommandResolveIDs { - StringPointer[] ptrs; - string files; -} - -struct WebsocketMessageManifestSuccess { +struct WebsocketCommandBuildWithFilePath { uint32 id; - string module_path; - Loader loader; - - DependencyManifest manifest; + string file_path; } -struct WebsocketMessageManifestFailure { +struct WebsocketMessageResolveID { uint32 id; - uint32 from_timestamp; - Loader loader; - Log log; -} +}
\ No newline at end of file diff --git a/src/api/schema.zig b/src/api/schema.zig index 2238ea3f0..8a850c8f1 100644 --- a/src/api/schema.zig +++ b/src/api/schema.zig @@ -2338,6 +2338,12 @@ pub const Api = struct { /// manifest_fail manifest_fail, + /// resolve_file + resolve_file, + + /// file_change_notification_with_hint + file_change_notification_with_hint, + _, pub fn jsonStringify(self: *const @This(), opts: anytype, o: anytype) !void { @@ -2353,6 +2359,9 @@ pub const Api = struct { /// manifest manifest, + /// build_with_file_path + build_with_file_path, + _, pub fn jsonStringify(self: *const @This(), opts: anytype, o: anytype) !void { @@ -2553,144 +2562,40 @@ pub const Api = struct { } }; - pub const DependencyManifest = struct { - /// ids - ids: []const u32, - - pub fn decode(reader: anytype) anyerror!DependencyManifest { - var this = std.mem.zeroes(DependencyManifest); - - this.ids = try reader.readArray(u32); - return this; - } - - pub fn encode(this: *const @This(), writer: anytype) anyerror!void { - try writer.writeArray(u32, this.ids); - } - }; - - pub const FileList = struct { - /// ptrs - ptrs: []const StringPointer, - - /// files - files: []const u8, - - pub fn decode(reader: anytype) anyerror!FileList { - var this = std.mem.zeroes(FileList); - - this.ptrs = try reader.readArray(StringPointer); - this.files = try reader.readValue([]const u8); - return this; - } - - pub fn encode(this: *const @This(), writer: anytype) anyerror!void { - try writer.writeArray(StringPointer, this.ptrs); - try writer.writeValue(@TypeOf(this.files), this.files); - } - }; - - pub const WebsocketMessageResolveIDs = struct { - /// id - id: []const u32, - - /// list - list: FileList, - - pub fn decode(reader: anytype) anyerror!WebsocketMessageResolveIDs { - var this = std.mem.zeroes(WebsocketMessageResolveIDs); - - this.id = try reader.readArray(u32); - this.list = try reader.readValue(FileList); - return this; - } - - pub fn encode(this: *const @This(), writer: anytype) anyerror!void { - try writer.writeArray(u32, this.id); - try writer.writeValue(@TypeOf(this.list), this.list); - } - }; - - pub const WebsocketCommandResolveIDs = struct { - /// ptrs - ptrs: []const StringPointer, - - /// files - files: []const u8, - - pub fn decode(reader: anytype) anyerror!WebsocketCommandResolveIDs { - var this = std.mem.zeroes(WebsocketCommandResolveIDs); - - this.ptrs = try reader.readArray(StringPointer); - this.files = try reader.readValue([]const u8); - return this; - } - - pub fn encode(this: *const @This(), writer: anytype) anyerror!void { - try writer.writeArray(StringPointer, this.ptrs); - try writer.writeValue(@TypeOf(this.files), this.files); - } - }; - - pub const WebsocketMessageManifestSuccess = struct { + pub const WebsocketCommandBuildWithFilePath = struct { /// id id: u32 = 0, - /// module_path - module_path: []const u8, + /// file_path + file_path: []const u8, - /// loader - loader: Loader, - - /// manifest - manifest: DependencyManifest, - - pub fn decode(reader: anytype) anyerror!WebsocketMessageManifestSuccess { - var this = std.mem.zeroes(WebsocketMessageManifestSuccess); + pub fn decode(reader: anytype) anyerror!WebsocketCommandBuildWithFilePath { + var this = std.mem.zeroes(WebsocketCommandBuildWithFilePath); this.id = try reader.readValue(u32); - this.module_path = try reader.readValue([]const u8); - this.loader = try reader.readValue(Loader); - this.manifest = try reader.readValue(DependencyManifest); + this.file_path = try reader.readValue([]const u8); return this; } pub fn encode(this: *const @This(), writer: anytype) anyerror!void { try writer.writeInt(this.id); - try writer.writeValue(@TypeOf(this.module_path), this.module_path); - try writer.writeEnum(this.loader); - try writer.writeValue(@TypeOf(this.manifest), this.manifest); + try writer.writeValue(@TypeOf(this.file_path), this.file_path); } }; - pub const WebsocketMessageManifestFailure = struct { + pub const WebsocketMessageResolveId = packed struct { /// id id: u32 = 0, - /// from_timestamp - from_timestamp: u32 = 0, - - /// loader - loader: Loader, - - /// log - log: Log, - - pub fn decode(reader: anytype) anyerror!WebsocketMessageManifestFailure { - var this = std.mem.zeroes(WebsocketMessageManifestFailure); + pub fn decode(reader: anytype) anyerror!WebsocketMessageResolveId { + var this = std.mem.zeroes(WebsocketMessageResolveId); this.id = try reader.readValue(u32); - this.from_timestamp = try reader.readValue(u32); - this.loader = try reader.readValue(Loader); - this.log = try reader.readValue(Log); return this; } pub fn encode(this: *const @This(), writer: anytype) anyerror!void { try writer.writeInt(this.id); - try writer.writeInt(this.from_timestamp); - try writer.writeEnum(this.loader); - try writer.writeValue(@TypeOf(this.log), this.log); } }; }; diff --git a/src/http.zig b/src/http.zig index c624c9bd6..09c9840e2 100644 --- a/src/http.zig +++ b/src/http.zig @@ -709,16 +709,7 @@ pub const RequestContext = struct { var watchlist_slice = this.watcher.watchlist.slice(); - const index = std.mem.indexOfScalar(u32, watchlist_slice.items(.hash), id) orelse { - - // log.addErrorFmt(null, logger.Loc.Empty, this, "File missing from watchlist: {d}. Please refresh :(", .{hash}) catch unreachable; - return WatchBuildResult{ - .value = .{ .fail = std.mem.zeroes(Api.WebsocketMessageBuildFailure) }, - .id = id, - .log = log, - .timestamp = WebsocketHandler.toTimestamp(Server.global_start_time.read()), - }; - }; + const index = std.mem.indexOfScalar(u32, watchlist_slice.items(.hash), id) orelse return error.MissingWatchID; const file_path_str = watchlist_slice.items(.file_path)[index]; const fd = watchlist_slice.items(.fd)[index]; @@ -1544,6 +1535,7 @@ pub const RequestContext = struct { // Output.prettyErrorln("<r><green>101<r><d> Hot Module Reloading connected.<r>", .{}); // Output.flush(); Analytics.Features.hot_module_reloading = true; + var build_file_path_buf: [std.fs.MAX_PATH_BYTES]u8 = undefined; var cmd: Api.WebsocketCommand = undefined; var msg: Api.WebsocketMessage = .{ @@ -1580,8 +1572,9 @@ pub const RequestContext = struct { Output.prettyErrorln("<r><red>ERR:<r> <b>Websocket failed to write.<r>", .{}); } } - while (!handler.tombstone) { + Output.flush(); + defer Output.flush(); handler.conn.client.getError() catch |err| { Output.prettyErrorln("<r><red>Websocket ERR:<r> <b>{s}<r>", .{err}); @@ -1617,13 +1610,74 @@ pub const RequestContext = struct { cmd_reader = ApiReader.init(cnst_frame, ctx.allocator); cmd = try Api.WebsocketCommand.decode(&cmd_reader); switch (cmd.kind) { - .build => { - var request = try Api.WebsocketCommandBuild.decode(&cmd_reader); + .build, .build_with_file_path => { + const request_id = if (cmd.kind == .build) + (try Api.WebsocketCommandBuild.decode(&cmd_reader)).id + else brk: { + const full_build = try Api.WebsocketCommandBuildWithFilePath.decode(&cmd_reader); + if (ctx.watcher.indexOf(full_build.id) != null) break :brk full_build.id; + const file_path = if (std.fs.path.isAbsolute(full_build.file_path)) + full_build.file_path + else + ctx.bundler.fs.absBuf( + &[_]string{ ctx.bundler.fs.top_level_dir, full_build.file_path }, + &build_file_path_buf, + ); + + if (Watcher.getHash(file_path) != full_build.id) { + Output.prettyErrorln("<r><red>ERR:<r> <b>File path hash mismatch for {s}.<r>", .{full_build.file_path}); + continue; + } + // save because WebSocket's buffer is 8096 + // max file path is 4096 + var path_buf = _global.constStrToU8(file_path); + path_buf.ptr[path_buf.len] = 0; + var file_path_z: [:0]u8 = path_buf.ptr[0..path_buf.len :0]; + const file = std.fs.openFileAbsoluteZ(file_path_z, .{ .read = true }) catch |err| { + Output.prettyErrorln("<r><red>ERR:<r>{s} opening file <b>{s}<r> <r>", .{ @errorName(err), full_build.file_path }); + continue; + }; + Fs.FileSystem.setMaxFd(file.handle); + try ctx.watcher.appendFile( + file.handle, + file_path, + full_build.id, + ctx.bundler.options.loader(Fs.PathName.init(file_path).ext), + 0, + null, + true, + ); + break :brk full_build.id; + }; var arena = std.heap.ArenaAllocator.init(default_allocator); defer arena.deinit(); - var build_result = try handler.builder.build(request.id, cmd.timestamp, arena.allocator()); + var head = Websocket.WebsocketHeader{ + .final = true, + .opcode = .Binary, + .mask = false, + .len = 0, + }; + + const build_result = handler.builder.build(request_id, cmd.timestamp, arena.allocator()) catch |err| { + if (err == error.MissingWatchID) { + msg.timestamp = cmd.timestamp; + msg.kind = Api.WebsocketMessageKind.resolve_file; + handler.message_buffer.reset(); + var buffer_writer = MutableStringAPIWriter.init(&handler.message_buffer); + try msg.encode(&buffer_writer); + _ = try handler.conn.client.write(handler.message_buffer.list.items, SOCKET_FLAGS); + const resolve_id = Api.WebsocketMessageResolveId{ .id = request_id }; + try resolve_id.encode(&buffer_writer); + head.len = Websocket.WebsocketHeader.packLength(handler.message_buffer.list.items.len); + try handler.websocket.writeHeader(head, handler.message_buffer.list.items.len); + _ = try handler.conn.client.write(handler.message_buffer.list.items, SOCKET_FLAGS); + continue; + } + + return err; + }; const file_path = switch (build_result.value) { .fail => |fail| fail.module_path, .success => |fail| fail.module_path, @@ -1652,39 +1706,35 @@ pub const RequestContext = struct { }, } - defer Output.flush(); - msg.timestamp = build_result.timestamp; - msg.kind = switch (build_result.value) { - .success => .build_success, - else => .build_fail, - }; - handler.message_buffer.reset(); - var buffer_writer = MutableStringAPIWriter.init(&handler.message_buffer); - try msg.encode(&buffer_writer); - var head = Websocket.WebsocketHeader{ - .final = true, - .opcode = .Binary, - .mask = false, - .len = 0, - }; - - switch (build_result.value) { - .success => |success| { - try success.encode(&buffer_writer); - const total = handler.message_buffer.list.items.len + build_result.bytes.len; - head.len = Websocket.WebsocketHeader.packLength(total); - try handler.websocket.writeHeader(head, total); - _ = try handler.conn.client.write(handler.message_buffer.list.items, SOCKET_FLAGS); - if (build_result.bytes.len > 0) { - _ = try handler.conn.client.write(build_result.bytes, SOCKET_FLAGS); - } - }, - .fail => |fail| { - try fail.encode(&buffer_writer); - head.len = Websocket.WebsocketHeader.packLength(handler.message_buffer.list.items.len); - try handler.websocket.writeHeader(head, handler.message_buffer.list.items.len); - _ = try handler.conn.client.write(handler.message_buffer.list.items, SOCKET_FLAGS); - }, + { + defer Output.flush(); + msg.timestamp = build_result.timestamp; + msg.kind = switch (build_result.value) { + .success => .build_success, + else => .build_fail, + }; + handler.message_buffer.reset(); + var buffer_writer = MutableStringAPIWriter.init(&handler.message_buffer); + try msg.encode(&buffer_writer); + + switch (build_result.value) { + .success => |success| { + try success.encode(&buffer_writer); + const total = handler.message_buffer.list.items.len + build_result.bytes.len; + head.len = Websocket.WebsocketHeader.packLength(total); + try handler.websocket.writeHeader(head, total); + _ = try handler.conn.client.write(handler.message_buffer.list.items, SOCKET_FLAGS); + if (build_result.bytes.len > 0) { + _ = try handler.conn.client.write(build_result.bytes, SOCKET_FLAGS); + } + }, + .fail => |fail| { + try fail.encode(&buffer_writer); + head.len = Websocket.WebsocketHeader.packLength(handler.message_buffer.list.items.len); + try handler.websocket.writeHeader(head, handler.message_buffer.list.items.len); + _ = try handler.conn.client.write(handler.message_buffer.list.items, SOCKET_FLAGS); + }, + } } }, else => { @@ -2176,8 +2226,12 @@ pub const RequestContext = struct { const path = ctx.url.path["bun:".len..]; if (strings.eqlComptime(path, "_api.hmr")) { - try ctx.handleWebsocket(server); - return; + if (ctx.header("Upgrade")) |upgrade| { + if (strings.eqlCaseInsensitiveASCII(upgrade.value, "websocket", true)) { + try ctx.handleWebsocket(server); + return; + } + } } if (strings.eqlComptime(path, "error.js")) { @@ -2482,23 +2536,25 @@ pub const Server = struct { fallback_only: bool = false, threadlocal var filechange_buf: [32]u8 = undefined; + threadlocal var filechange_buf_hinted: [32]u8 = undefined; pub fn onFileUpdate( ctx: *Server, events: []watcher.WatchEvent, + changed_files: []?[:0]u8, watchlist: watcher.Watchlist, ) void { if (ctx.javascript_enabled) { if (Output.isEmojiEnabled()) { - _onFileUpdate(ctx, events, watchlist, true, true); + _onFileUpdate(ctx, events, changed_files, watchlist, true, true); } else { - _onFileUpdate(ctx, events, watchlist, true, false); + _onFileUpdate(ctx, events, changed_files, watchlist, true, false); } } else { if (Output.isEmojiEnabled()) { - _onFileUpdate(ctx, events, watchlist, false, true); + _onFileUpdate(ctx, events, changed_files, watchlist, false, true); } else { - _onFileUpdate(ctx, events, watchlist, false, false); + _onFileUpdate(ctx, events, changed_files, watchlist, false, false); } } } @@ -2506,21 +2562,38 @@ pub const Server = struct { fn _onFileUpdate( ctx: *Server, events: []watcher.WatchEvent, + changed_files: []?[:0]u8, watchlist: watcher.Watchlist, comptime is_javascript_enabled: bool, comptime is_emoji_enabled: bool, ) void { var fbs = std.io.fixedBufferStream(&filechange_buf); - var writer = ByteApiWriter.init(&fbs); - const message_type = Api.WebsocketMessage{ - .timestamp = RequestContext.WebsocketHandler.toTimestamp(ctx.timer.read()), - .kind = .file_change_notification, - }; - message_type.encode(&writer) catch unreachable; + var hinted_fbs = std.io.fixedBufferStream(&filechange_buf_hinted); + { + var writer = ByteApiWriter.init(&fbs); + const message_type = Api.WebsocketMessage{ + .timestamp = RequestContext.WebsocketHandler.toTimestamp(ctx.timer.read()), + .kind = .file_change_notification, + }; + + message_type.encode(&writer) catch unreachable; + } + + { + var writer = ByteApiWriter.init(&hinted_fbs); + const message_type = Api.WebsocketMessage{ + .timestamp = RequestContext.WebsocketHandler.toTimestamp(ctx.timer.read()), + .kind = Api.WebsocketMessageKind.file_change_notification_with_hint, + }; + + message_type.encode(&writer) catch unreachable; + } + var slice = watchlist.slice(); const file_paths = slice.items(.file_path); var counts = slice.items(.count); const kinds = slice.items(.kind); + const hashes = slice.items(.hash); var header = fbs.getWritten(); defer ctx.watcher.flushEvictions(); defer Output.flush(); @@ -2538,8 +2611,9 @@ pub const Server = struct { // so it's consistent with the rest // if we use .extname we might run into an issue with whether or not the "." is included. const path = Fs.PathName.init(file_path); - const id = watchlist.items(.hash)[event.index]; + const id = hashes[event.index]; var content_fbs = std.io.fixedBufferStream(filechange_buf[header.len..]); + var hinted_content_fbs = std.io.fixedBufferStream(filechange_buf_hinted[header.len..]); defer { if (comptime is_javascript_enabled) { @@ -2548,6 +2622,10 @@ pub const Server = struct { } } + if (comptime Environment.isDebug) { + Output.prettyErrorln("[watcher] {s}: -- {}", .{ @tagName(kind), event.op }); + } + switch (kind) { .file => { if (event.op.delete or event.op.rename) { @@ -2562,11 +2640,6 @@ pub const Server = struct { Output.prettyErrorln("<r><d>File changed: {s}<r>", .{ctx.bundler.fs.relativeTo(file_path)}); } } else { - if (event.op.move) { - var fds = ctx.watcher.watchlist.items(.fd); - fds[event.index] = 0; - } - const change_message = Api.WebsocketMessageFileChangeNotification{ .id = id, .loader = (ctx.bundler.options.loaders.get(path.ext) orelse .file).toAPI(), @@ -2587,12 +2660,68 @@ pub const Server = struct { } }, .directory => { + const affected = event.names(changed_files); + + if (affected.len > 0) { + if (rfs.entries.get(file_path)) |dir_ent| { + var last_file_hash: Watcher.HashType = std.math.maxInt(Watcher.HashType); + var already_had_all_affected = true; + for (affected) |changed_name_ptr| { + const changed_name: []const u8 = std.mem.span((changed_name_ptr orelse continue)); + const loader = (ctx.bundler.options.loaders.get(Fs.PathName.init(changed_name).ext) orelse .file); + if (loader.isJavaScriptLikeOrJSON() or loader == .css) { + if (dir_ent.entries.get(changed_name)) |file_ent| { + const abs_path = file_ent.entry.abs_path.slice(); + const file_hash = Watcher.getHash(abs_path); + + // skip consecutive duplicates + if (last_file_hash == file_hash) continue; + last_file_hash = file_hash; + + // reset the file descriptor + file_ent.entry.cache.fd = 0; + file_ent.entry.need_stat = true; + + const change_message = Api.WebsocketMessageFileChangeNotification{ + .id = file_hash, + .loader = loader.toAPI(), + }; + + var content_writer = ByteApiWriter.init(&hinted_content_fbs); + change_message.encode(&content_writer) catch unreachable; + const change_buf = hinted_content_fbs.getWritten(); + const written_buf = filechange_buf_hinted[0 .. header.len + change_buf.len]; + RequestContext.WebsocketHandler.broadcast(written_buf) catch |err| { + Output.prettyErrorln("Error writing change notification: {s}<r>", .{@errorName(err)}); + }; + if (comptime is_emoji_enabled) { + Output.prettyErrorln("<r>📜 <d>File change: {s}<r>", .{ctx.bundler.fs.relativeTo(abs_path)}); + } else { + Output.prettyErrorln("<r> <d>File change: {s}<r>", .{ctx.bundler.fs.relativeTo(abs_path)}); + } + } else { + already_had_all_affected = false; + } + } + } + + // When the only operation in a directory was moving new files into it, and we were already watching the existing files + // We don't need to invalidate the directory entries + // We only need to invalidate the file descriptor + if (already_had_all_affected and event.op.move_to and !event.op.delete and + !event.op.rename and + !event.op.write) + { + continue; + } + } + } + rfs.bustEntriesCache(file_path); ctx.bundler.resolver.dir_cache.remove(file_path); // if (event.op.delete or event.op.rename) // ctx.watcher.removeAtIndex(event.index, hashes[event.index], parent_hashes, .directory); - if (comptime is_emoji_enabled) { Output.prettyErrorln("<r>📁 <d>Dir change: {s}<r>", .{ctx.bundler.fs.relativeTo(file_path)}); } else { diff --git a/src/http/websocket.zig b/src/http/websocket.zig index caa511670..01ddb8bc8 100644 --- a/src/http/websocket.zig +++ b/src/http/websocket.zig @@ -112,7 +112,7 @@ pub const Websocket = struct { conn: *tcp.Connection, err: ?anyerror = null, - buf: [4096]u8 = undefined, + buf: [8096]u8 = undefined, read_stream: ReadStream, reader: ReadStream.Reader, flags: u32 = 0, diff --git a/src/http_client_async.zig b/src/http_client_async.zig index 6c3d9585a..947e8a944 100644 --- a/src/http_client_async.zig +++ b/src/http_client_async.zig @@ -22,7 +22,7 @@ const AsyncIO = @import("io"); const ThreadPool = @import("thread_pool"); const boring = @import("boringssl"); pub const NetworkThread = @import("./network_thread.zig"); - +const ObjectPool = @import("./pool.zig").ObjectPool; const SOCK = os.SOCK; pub const Headers = struct { @@ -222,11 +222,21 @@ pub const HeaderBuilder = struct { }; pub const HTTPChannel = @import("./sync.zig").Channel(*AsyncHTTP, .{ .Static = 1000 }); - // 32 pointers much cheaper than 1000 pointers -const SingleHTTPChannel = @import("./sync.zig").Channel(*AsyncHTTP, .{ .Static = 32 }); -var send_sync_channel: SingleHTTPChannel = undefined; -var send_sync_channel_loaded: bool = false; +const SingleHTTPChannel = struct { + const SingleHTTPCHannel_ = @import("./sync.zig").Channel(*AsyncHTTP, .{ .Static = 8 }); + channel: SingleHTTPCHannel_, + pub fn reset(_: *@This()) void { + + } + pub fn init(_: std.mem.Allocator) anyerror!SingleHTTPChannel { + return SingleHTTPChannel{ + .channel = SingleHTTPCHannel_.init() + }; + } +}; + +const SingleHTTPChannelPool = ObjectPool(SingleHTTPChannel, SingleHTTPChannel.init, false); pub const HTTPChannelContext = struct { http: AsyncHTTP = undefined, @@ -270,6 +280,7 @@ pub const AsyncHTTP = struct { /// Callback runs when request finishes /// Executes on the network thread callback: ?CompletionCallback = null, + callback_ctx: ?*anyopaque = null, pub const CompletionCallback = fn (this: *AsyncHTTP, sender: *HTTPSender) void; pub var active_requests_count = std.atomic.Atomic(u32).init(0); @@ -317,22 +328,26 @@ pub const AsyncHTTP = struct { } fn sendSyncCallback(this: *AsyncHTTP, sender: *HTTPSender) void { - send_sync_channel.writeItem(this) catch unreachable; + var pooled_node = @ptrCast(*SingleHTTPChannelPool.Node, @alignCast(@alignOf(*SingleHTTPChannelPool.Node), this.callback_ctx.?)); + pooled_node.data.channel.writeItem(this) catch unreachable; sender.release(); } pub fn sendSync(this: *AsyncHTTP) anyerror!picohttp.Response { - if (!send_sync_channel_loaded) { - send_sync_channel_loaded = true; - send_sync_channel = SingleHTTPChannel.init(); + this.callback_ctx = SingleHTTPChannelPool.get(default_allocator); + defer { + var pooled_node = @ptrCast(*SingleHTTPChannelPool.Node, @alignCast(@alignOf(*SingleHTTPChannelPool.Node), this.callback_ctx.?)); + SingleHTTPChannelPool.release(pooled_node); + this.callback_ctx = null; } - this.callback = sendSyncCallback; + var batch = NetworkThread.Batch{}; this.schedule(this.allocator, &batch); NetworkThread.global.pool.schedule(batch); while (true) { - var async_http: *AsyncHTTP = (send_sync_channel.tryReadItem() catch unreachable) orelse { + var pooled = @ptrCast(*SingleHTTPChannelPool.Node, @alignCast(@alignOf(*SingleHTTPChannelPool.Node), this.callback_ctx.?)); + var async_http: *AsyncHTTP = (pooled.data.channel.tryReadItem() catch unreachable) orelse { std.atomic.spinLoopHint(); std.time.sleep(std.time.ns_per_us * 100); continue; diff --git a/src/runtime/hmr.ts b/src/runtime/hmr.ts index 3803dff2e..965339f80 100644 --- a/src/runtime/hmr.ts +++ b/src/runtime/hmr.ts @@ -3,6 +3,7 @@ import * as API from "../api/schema"; var __HMRModule, __FastRefreshModule, __HMRClient, __injectFastRefresh; if (typeof window !== "undefined") { + var textEncoder: TextEncoder; // We add a scope here to minimize chances of namespace collisions var runOnce = false; var clientStartTime = 0; @@ -185,7 +186,7 @@ if (typeof window !== "undefined") { return CSSLoader.cssLoadId.bundle_id; } - private findCSSLinkTag(id: number): CSSHMRInsertionPoint | null { + findCSSLinkTag(id: number): CSSHMRInsertionPoint | null { let count = 0; let match: CSSHMRInsertionPoint = null; @@ -327,6 +328,7 @@ if (typeof window !== "undefined") { } let filepath = update.file; + // We cannot safely do this because the hash would change on the server if (filepath.startsWith(this.hmr.cwd)) { filepath = filepath.substring(this.hmr.cwd.length); } @@ -846,10 +848,10 @@ if (typeof window !== "undefined") { } } - handleFileChangeNotification(buffer: ByteBuffer, timestamp: number) { + handleFileChangeNotification(buffer: ByteBuffer, timestamp: number, copy_file_path: boolean) { const notification = API.decodeWebsocketMessageFileChangeNotification(buffer); - let file_path = ""; + let file_path = ""; switch (notification.loader) { case API.Loader.css: { file_path = this.loaders.css.filePath(notification); @@ -866,6 +868,10 @@ if (typeof window !== "undefined") { } } + return this.handleFileChangeNotificationBase(timestamp, notification, file_path, copy_file_path); + } + + private handleFileChangeNotificationBase(timestamp: number, notification: API.WebsocketMessageFileChangeNotification, file_path: string, copy_file_path: boolean) { const accept = file_path && file_path.length > 0; if (!accept) { @@ -923,12 +929,34 @@ if (typeof window !== "undefined") { switch (reloadBehavior) { // This is the same command/logic for both JS and CSS hot reloading. case ReloadBehavior.hotReload: { - this.buildCommandBuf[0] = API.WebsocketCommandKind.build; + if (copy_file_path && !this.buildCommandBufWithFilePath) { + // on Linux, max file path length is 4096 bytes + // on macOS & Windows, max file path length is 1024 bytes + // 256 is extra breathing room + this.buildCommandBufWithFilePath = new Uint8Array(4096 + 256); + } + + const writeBuffer = !copy_file_path ? this.buildCommandBuf : this.buildCommandBufWithFilePath; + writeBuffer[0] = !copy_file_path ? API.WebsocketCommandKind.build : API.WebsocketCommandKind.build_with_file_path; this.buildCommandUArray[0] = timestamp; - this.buildCommandBuf.set(this.buildCommandUArrayEight, 1); + writeBuffer.set(this.buildCommandUArrayEight, 1); this.buildCommandUArray[0] = notification.id; - this.buildCommandBuf.set(this.buildCommandUArrayEight, 5); - this.socket.send(this.buildCommandBuf); + writeBuffer.set(this.buildCommandUArrayEight, 5); + + if (copy_file_path) { + if (!textEncoder) { + textEncoder = new TextEncoder(); + } + + this.buildCommandUArray[0] = file_path.length; + writeBuffer.set(this.buildCommandUArrayEight, 9); + + const out = textEncoder.encodeInto(file_path, writeBuffer.subarray(13)); + this.socket.send(this.buildCommandBufWithFilePath.subarray(0, 13 + out.written)); + } else { + this.socket.send(this.buildCommandBuf); + } + if (this.verbose) { __hmrlog.debug(`Requesting update for ${file_path}`); } @@ -941,10 +969,14 @@ if (typeof window !== "undefined") { } } } + buildCommandBuf = new Uint8Array(9); buildCommandUArray = new Uint32Array(1); buildCommandUArrayEight = new Uint8Array(this.buildCommandUArray.buffer); + // lazily allocate because it's going to be much larger than 9 bytes + buildCommandBufWithFilePath: Uint8Array; + // On open, reset the delay for reconnecting handleOpen = (event: Event) => { globalThis.clearTimeout(this.nextReconnectAttempt); @@ -974,8 +1006,84 @@ if (typeof window !== "undefined") { break; } + case API.WebsocketMessageKind.resolve_file: { + const {id} = API.decodeWebsocketMessageResolveID(buffer); + const timestamp = this.builds.get(id) || 0; + + if (timestamp == 0 && HotReload.VERBOSE) { + __hmrlog.debug(`Unknown module? ${id}`); + return; + } + + const index = HMRModule.dependencies.graph.indexOf(id); + var file_path: string = ""; + var loader = API.Loader.js; + if (index > -1) { + file_path = HMRModule.dependencies.modules[index].file_path; + } else { + const tag = this.loaders.css.findCSSLinkTag(id); + if (tag && tag.file.length) { + file_path = tag.file; + } + } + + if (!file_path || file_path.length === 0) { + if (HotReload.VERBOSE) { + __hmrlog.debug(`Unknown module? ${id}`); + } + return; + } + + switch (file_path.substring(file_path.lastIndexOf("."))) { + case ".css": { + loader = API.Loader.css; + break; + } + + case ".mjs": + case ".cjs": + case ".js": { + loader = API.Loader.js; + break; + } + + case ".json": { + loader = API.Loader.json; + break; + } + + case ".cts": + case ".mts": + case ".ts": { + loader = API.Loader.ts; + break; + } + + case ".tsx": { + loader = API.Loader.tsx; + break; + } + + case ".jsx": { + loader = API.Loader.jsx; + break; + } + + default: { + loader = API.Loader.file; + break; + } + } + + this.handleFileChangeNotificationBase(timestamp, {id, loader}, file_path, true); + break; + } case API.WebsocketMessageKind.file_change_notification: { - this.handleFileChangeNotification(buffer, header.timestamp); + this.handleFileChangeNotification(buffer, header.timestamp, false); + break; + } + case API.WebsocketMessageKind.file_change_notification_with_hint: { + this.handleFileChangeNotification(buffer, header.timestamp, true); break; } case API.WebsocketMessageKind.welcome: { diff --git a/src/watcher.zig b/src/watcher.zig index 7aa5c2ccf..d97be28a1 100644 --- a/src/watcher.zig +++ b/src/watcher.zig @@ -20,7 +20,7 @@ const os = std.os; const Mutex = @import("./lock.zig").Lock; const Futex = @import("./futex.zig"); -const WatchItemIndex = u16; +pub const WatchItemIndex = u16; const NoWatchItem: WatchItemIndex = std.math.maxInt(WatchItemIndex); const PackageJSON = @import("./resolver/package_json.zig").PackageJSON; @@ -65,6 +65,16 @@ pub const INotify = struct { mask: u32, cookie: u32, name_len: u32, + + pub fn name(this: *const INotifyEvent) [:0]u8 { + if (comptime Environment.allow_assert) std.debug.assert(this.name_len > 0); + + // the name_len field is wrong + // it includes alignment / padding + // but it is a sentineled value + // so we can just trim it to the first null byte + return std.mem.sliceTo(@intToPtr([*]u8, @ptrToInt(this) + @sizeOf(INotifyEvent))[0..this.name_len:0], 0); + } }; pub var inotify_fd: EventListIndex = 0; pub var loaded_inotify = false; @@ -208,6 +218,13 @@ pub const WatchItem = struct { pub const WatchEvent = struct { index: WatchItemIndex, op: Op, + name_off: u8 = 0, + name_len: u8 = 0, + + pub fn names(this: WatchEvent, buf: []?[:0]u8) []?[:0]u8 { + if (this.name_len == 0) return &[_]?[:0]u8{}; + return buf[this.name_off..][0..this.name_len]; + } const KEvent = std.c.Kevent; @@ -218,15 +235,17 @@ pub const WatchEvent = struct { } pub fn merge(this: *WatchEvent, other: WatchEvent) void { + this.name_len += other.name_len; this.op = Op{ .delete = this.op.delete or other.op.delete, .metadata = this.op.metadata or other.op.metadata, .rename = this.op.rename or other.op.rename, - .move = this.op.move or other.op.move, .write = this.op.write or other.op.write, }; } + + pub fn fromKEvent(this: *WatchEvent, kevent: KEvent) void { this.* = WatchEvent{ @@ -234,20 +253,21 @@ pub const WatchEvent = struct { .delete = (kevent.fflags & std.c.NOTE_DELETE) > 0, .metadata = (kevent.fflags & std.c.NOTE_ATTRIB) > 0, .rename = (kevent.fflags & std.c.NOTE_RENAME) > 0, - .move = false, // unhandled .write = (kevent.fflags & std.c.NOTE_WRITE) > 0, }, .index = @truncate(WatchItemIndex, kevent.udata), }; } + + pub fn fromINotify(this: *WatchEvent, event: INotify.INotifyEvent, index: WatchItemIndex) void { this.* = WatchEvent{ .op = Op{ .delete = (event.mask & INotify.IN_DELETE_SELF) > 0 or (event.mask & INotify.IN_DELETE) > 0, .metadata = false, .rename = (event.mask & INotify.IN_MOVE_SELF) > 0, - .move = (event.mask & INotify.IN_MOVED_TO) > 0, + .move_to = (event.mask & INotify.IN_MOVED_TO) > 0, .write = (event.mask & INotify.IN_MODIFY) > 0, }, .index = index, @@ -255,12 +275,16 @@ pub const WatchEvent = struct { } pub const Op = packed struct { + padding: u3 = 0, + delete: bool = false, metadata: bool = false, rename: bool = false, write: bool = false, - move: bool = false, + move_to: bool = false, }; + + }; pub const Watchlist = std.MultiArrayList(WatchItem); @@ -281,6 +305,7 @@ pub fn NewWatcher(comptime ContextType: type) type { // User-facing watch_events: [128]WatchEvent = undefined, + changed_filepaths: [128]?[:0]u8 = std.mem.zeroes([128]?[:0]u8), fs: *Fs.FileSystem, // this is what kqueue knows about @@ -424,7 +449,7 @@ pub fn NewWatcher(comptime ContextType: type) type { watchevents[i].fromKEvent(event); } - this.ctx.onFileUpdate(watchevents, this.watchlist); + this.ctx.onFileUpdate(watchevents, this.changed_filepaths[0..watchevents.len], this.watchlist); } } else if (Environment.isLinux) { restart: while (true) { @@ -434,9 +459,12 @@ pub fn NewWatcher(comptime ContextType: type) type { // TODO: is this thread safe? const eventlist_index = this.watchlist.items(.eventlist_index); var remaining_events = events.len; + var name_off: u8 = 0; + var temp_name_list: [128]?[:0]u8 = undefined; + var temp_name_off: u8 = 0; while (remaining_events > 0) { - const slice = events[0..std.math.min(remaining_events, this.watch_events.len)]; + const slice = events[0..@minimum(remaining_events, this.watch_events.len)]; var watchevents = this.watch_events[0..slice.len]; var watch_event_id: u32 = 0; for (slice) |event| { @@ -451,7 +479,14 @@ pub fn NewWatcher(comptime ContextType: type) type { ) orelse continue, ), ); - + temp_name_list[temp_name_off] = if (event.name_len > 0) + event.name() + else + null; + watchevents[watch_event_id].name_off = temp_name_off; + watchevents[watch_event_id].name_len = @as(u8, @boolToInt((event.name_len > 0))); + temp_name_off += @as(u8, @boolToInt((event.name_len > 0))); + watch_event_id += 1; } @@ -460,16 +495,25 @@ pub fn NewWatcher(comptime ContextType: type) type { var last_event_index: usize = 0; var last_event_id: INotify.EventListIndex = std.math.maxInt(INotify.EventListIndex); - for (all_events) |event, i| { - if (event.index == last_event_id) { - all_events[last_event_index].merge(event); + + + + for (all_events) |_, i| { + if (all_events[i].name_len > 0) { + this.changed_filepaths[name_off] = temp_name_list[all_events[i].name_off]; + all_events[i].name_off = name_off; + name_off += 1; + } + + if (all_events[i].index == last_event_id) { + all_events[last_event_index].merge(all_events[i]); continue; } last_event_index = i; - last_event_id = event.index; + last_event_id = all_events[i].index; } if (all_events.len == 0) continue :restart; - this.ctx.onFileUpdate(all_events[0 .. last_event_index + 1], this.watchlist); + this.ctx.onFileUpdate(all_events[0 .. last_event_index + 1], this.changed_filepaths[0..name_off + 1], this.watchlist); remaining_events -= slice.len; } } |