electrobun 1.18.1 → 1.18.4-beta.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 CHANGED
@@ -66,6 +66,7 @@ Don't miss our:
66
66
  - [Marginalia](https://github.com/lars-hoeijmans/Marginalia) - a simple note taking app
67
67
  - [MarkBun](https://github.com/xiaochong/markbun) - fast, beautiful, Typora-like markdown desktop editor
68
68
  - [md-browse](https://github.com/needle-tools/md-browse) - a markdown-first browser that converts web pages to clean markdown
69
+ - [moop](https://github.com/zrubinrattet/moop/) - desktop app for batch image optimization for the web
69
70
  - [Patchline](https://github.com/adwaithks/Patchline) - lightweight desktop Git client for reading patches and line diffs, then staging and committing changes
70
71
  - [peekachu](https://github.com/needle-tools/peekachu) - password manager for AIs; store secrets in your OS keychain and scrub output so AI assistants never see actual values
71
72
  - [PiBun](https://github.com/khairold/pibun) - desktop GUI for the Pi coding agent with chat, terminal, git integration, and plugin system
@@ -112,6 +112,14 @@ export interface ElectrobunConfig {
112
112
  * Build configuration options
113
113
  */
114
114
  build?: {
115
+ /**
116
+ * Main process implementation to build and package.
117
+ * - "bun": bundle and run the Bun main process entrypoint
118
+ * - "zig": compile and run the Zig main process entrypoint
119
+ * @default "bun"
120
+ */
121
+ mainProcess?: "bun" | "zig";
122
+
115
123
  /**
116
124
  * Bun process build configuration.
117
125
  * Accepts all Bun.build() options (plugins, sourcemap, minify, define, etc.)
@@ -125,6 +133,18 @@ export interface ElectrobunConfig {
125
133
  entrypoint?: string;
126
134
  } & BunBuildOptions;
127
135
 
136
+ /**
137
+ * Zig main process build configuration.
138
+ * Used when `build.mainProcess` is set to `"zig"`.
139
+ */
140
+ zig?: {
141
+ /**
142
+ * Entry point for the main Zig process
143
+ * @default "src/zig/main.zig"
144
+ */
145
+ entrypoint?: string;
146
+ };
147
+
128
148
  /**
129
149
  * Browser view build configurations.
130
150
  * Each view accepts all Bun.build() options (plugins, sourcemap, minify, define, etc.)
@@ -1,5 +1,4 @@
1
- import { native, toCString, ffi } from "../proc/native";
2
- import * as fs from "fs";
1
+ import { ffi } from "../proc/native";
3
2
  import electrobunEventEmitter from "../events/eventEmitter";
4
3
  import {
5
4
  type ElectrobunRPCSchema,
@@ -7,10 +6,8 @@ import {
7
6
  type RPCWithTransport,
8
7
  defineElectrobunRPC,
9
8
  } from "../../shared/rpc.js";
10
- import { Updater } from "./Updater";
11
9
  import { BuildConfig } from "./BuildConfig";
12
10
  import {
13
- rpcPort,
14
11
  sendMessageToWebviewViaSocket,
15
12
  removeSocketForWebview,
16
13
  } from "./Socket";
@@ -20,7 +17,6 @@ import { type Pointer } from "bun:ffi";
20
17
  const BrowserViewMap: {
21
18
  [id: number]: BrowserView<any>;
22
19
  } = {};
23
- let nextWebviewId = 1;
24
20
 
25
21
  export type BrowserViewOptions<T = undefined> = {
26
22
  url: string | null;
@@ -38,7 +34,6 @@ export type BrowserViewOptions<T = undefined> = {
38
34
  rpc: T;
39
35
  hostWebviewId: number;
40
36
  autoResize: boolean;
41
-
42
37
  windowId: number;
43
38
  navigationRules: string | null;
44
39
  // Sandbox mode: when true, disables RPC and only allows event emission
@@ -52,8 +47,7 @@ export type BrowserViewOptions<T = undefined> = {
52
47
  // renderer:
53
48
  };
54
49
 
55
- const hash = await Updater.localInfo.hash();
56
- const buildConfig = await BuildConfig.get();
50
+ const buildConfig = BuildConfig.getSync();
57
51
 
58
52
  const defaultOptions: Partial<BrowserViewOptions> = {
59
53
  url: null,
@@ -68,13 +62,8 @@ const defaultOptions: Partial<BrowserViewOptions> = {
68
62
  height: 600,
69
63
  },
70
64
  };
71
- // Note: we use the build's hash to separate from different apps and different builds
72
- // but we also want a randomId to separate different instances of the same app
73
- const randomId = Math.random().toString(36).substring(7);
74
-
75
65
  export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
76
- id: number = nextWebviewId++;
77
- ptr: Pointer | null = null;
66
+ id = 0;
78
67
  hostWebviewId?: number;
79
68
  windowId!: number;
80
69
  renderer!: "cef" | "native";
@@ -95,9 +84,6 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
95
84
  width: 800,
96
85
  height: 600,
97
86
  };
98
- pipePrefix!: string;
99
- inStream!: fs.WriteStream;
100
- outStream!: ReadableStream<Uint8Array>;
101
87
  secretKey!: Uint8Array;
102
88
  rpc?: T;
103
89
  rpcHandler?: (msg: unknown) => void;
@@ -108,6 +94,13 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
108
94
  startPassthrough: boolean = false;
109
95
  isRemoved: boolean = false;
110
96
 
97
+ get ptr(): Pointer | null {
98
+ if (this.isRemoved) {
99
+ return null;
100
+ }
101
+ return ffi.request.getWebviewPointer({ id: this.id }) as Pointer | null;
102
+ }
103
+
111
104
  constructor(options: Partial<BrowserViewOptions<T>> = defaultOptions) {
112
105
  // const rpc = options.rpc;
113
106
 
@@ -124,9 +117,6 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
124
117
  this.rpc = options.rpc;
125
118
  this.secretKey = new Uint8Array(randomBytes(32));
126
119
  this.partition = options.partition || null;
127
- // todo (yoav): since collisions can crash the app add a function that checks if the
128
- // file exists first
129
- this.pipePrefix = `/private/tmp/electrobun_ipc_pipe_${hash}_${randomId}_${this.id}`;
130
120
  this.hostWebviewId = options.hostWebviewId;
131
121
  this.windowId = options.windowId ?? 0;
132
122
  this.autoResize = options.autoResize === false ? false : true;
@@ -136,8 +126,8 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
136
126
  this.startTransparent = options.startTransparent ?? false;
137
127
  this.startPassthrough = options.startPassthrough ?? false;
138
128
 
129
+ this.id = this.init() as number;
139
130
  BrowserViewMap[this.id] = this;
140
- this.ptr = this.init() as Pointer;
141
131
 
142
132
  // If HTML content was provided, load it after webview creation.
143
133
  if (this.html) {
@@ -148,21 +138,17 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
148
138
  }
149
139
 
150
140
  init() {
151
- this.createStreams();
141
+ this.initializeRpcTransport();
152
142
 
153
143
  return ffi.request.createWebview({
154
- id: this.id,
155
144
  windowId: this.windowId,
145
+ hostWebviewId: this.hostWebviewId ?? null,
156
146
  renderer: this.renderer,
157
- rpcPort: rpcPort,
158
147
  // todo: consider sending secretKey as base64
159
148
  secretKey: this.secretKey.toString(),
160
- hostWebviewId: this.hostWebviewId || null,
161
- pipePrefix: this.pipePrefix,
162
149
  partition: this.partition,
163
150
  // Only pass URL if no HTML content is provided to avoid conflicts
164
151
  url: this.html ? null : this.url,
165
- html: this.html,
166
152
  preload: this.preload,
167
153
  viewsRoot: this.viewsRoot,
168
154
  frame: {
@@ -180,7 +166,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
180
166
  });
181
167
  }
182
168
 
183
- createStreams() {
169
+ initializeRpcTransport() {
184
170
  if (!this.rpc) {
185
171
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
186
172
  this.rpc = BrowserView.defineRPC({
@@ -224,7 +210,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
224
210
 
225
211
  loadURL(url: string) {
226
212
  this.url = url;
227
- native!.symbols.loadURLInWebView(this.ptr, toCString(this.url));
213
+ ffi.request.loadURLInWebView({ id: this.id, url: this.url });
228
214
  }
229
215
 
230
216
  loadHTML(html: string) {
@@ -232,18 +218,18 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
232
218
 
233
219
  if (this.renderer === "cef") {
234
220
  // For CEF, store HTML content in native map and use scheme handler
235
- native!.symbols.setWebviewHTMLContent(this.id, toCString(html));
221
+ ffi.request.setWebviewHTMLContent({ id: this.id, html });
236
222
  this.loadURL("views://internal/index.html");
237
223
  } else {
238
224
  // For WKWebView, load HTML content directly
239
- native!.symbols.loadHTMLInWebView(this.ptr, toCString(html));
225
+ ffi.request.loadHTMLInWebView({ id: this.id, html });
240
226
  }
241
227
  }
242
228
 
243
229
  setNavigationRules(rules: string[]) {
244
230
  this.navigationRules = JSON.stringify(rules);
245
231
  const rulesJson = JSON.stringify(rules);
246
- native!.symbols.setWebviewNavigationRules(this.ptr, toCString(rulesJson));
232
+ ffi.request.setWebviewNavigationRules({ id: this.id, rulesJson });
247
233
  }
248
234
 
249
235
  findInPage(
@@ -252,28 +238,28 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
252
238
  ) {
253
239
  const forward = options?.forward ?? true;
254
240
  const matchCase = options?.matchCase ?? false;
255
- native!.symbols.webviewFindInPage(
256
- this.ptr,
257
- toCString(searchText),
241
+ ffi.request.webviewFindInPage({
242
+ id: this.id,
243
+ searchText,
258
244
  forward,
259
245
  matchCase,
260
- );
246
+ });
261
247
  }
262
248
 
263
249
  stopFindInPage() {
264
- native!.symbols.webviewStopFind(this.ptr);
250
+ ffi.request.webviewStopFind({ id: this.id });
265
251
  }
266
252
 
267
253
  openDevTools() {
268
- native!.symbols.webviewOpenDevTools(this.ptr);
254
+ ffi.request.webviewOpenDevTools({ id: this.id });
269
255
  }
270
256
 
271
257
  closeDevTools() {
272
- native!.symbols.webviewCloseDevTools(this.ptr);
258
+ ffi.request.webviewCloseDevTools({ id: this.id });
273
259
  }
274
260
 
275
261
  toggleDevTools() {
276
- native!.symbols.webviewToggleDevTools(this.ptr);
262
+ ffi.request.webviewToggleDevTools({ id: this.id });
277
263
  }
278
264
 
279
265
  /**
@@ -281,7 +267,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
281
267
  * @param zoomLevel - The zoom level (1.0 = 100%, 1.5 = 150%, etc.)
282
268
  */
283
269
  setPageZoom(zoomLevel: number) {
284
- native!.symbols.webviewSetPageZoom(this.ptr, zoomLevel);
270
+ ffi.request.webviewSetPageZoom({ id: this.id, zoomLevel });
285
271
  }
286
272
 
287
273
  /**
@@ -289,7 +275,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
289
275
  * @returns The current zoom level (1.0 = 100%)
290
276
  */
291
277
  getPageZoom(): number {
292
- return native!.symbols.webviewGetPageZoom(this.ptr) as number;
278
+ return ffi.request.webviewGetPageZoom({ id: this.id }) as number;
293
279
  }
294
280
 
295
281
  // todo (yoav): move this to a class that also has off, append, prepend, etc.
@@ -345,7 +331,6 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
345
331
  if (!this.ptr || this.isRemoved) {
346
332
  return;
347
333
  }
348
- const ptr = this.ptr;
349
334
  this.isRemoved = true;
350
335
  // Drop JS-side references first so late callbacks cannot target a stale view.
351
336
  delete BrowserViewMap[this.id];
@@ -356,10 +341,7 @@ export class BrowserView<T extends RPCWithTransport = RPCWithTransport> {
356
341
  unregisterHandler() {},
357
342
  });
358
343
  this.rpcHandler = undefined;
359
-
360
- this.rpcHandler = undefined;
361
- this.ptr = null;
362
- native!.symbols.webviewRemove(ptr);
344
+ ffi.request.webviewRemove({ id: this.id });
363
345
  }
364
346
 
365
347
  static getById(id: number) {
@@ -5,11 +5,10 @@ import { type Pointer } from "bun:ffi";
5
5
  import { BuildConfig } from "./BuildConfig";
6
6
  import { quit } from "./Utils";
7
7
  import { type RPCWithTransport } from "../../shared/rpc.js";
8
- import { getNextWindowId } from "./windowIds";
9
8
  import { GpuWindowMap } from "./GpuWindow";
10
9
  import { WGPUView } from "./WGPUView";
11
10
 
12
- const buildConfig = await BuildConfig.get();
11
+ const buildConfig = BuildConfig.getSync();
13
12
 
14
13
  export type WindowOptionsType<T = undefined> = {
15
14
  trafficLightOffset?: {
@@ -116,8 +115,7 @@ electrobunEventEmitter.on("close", (event: { data: { id: number } }) => {
116
115
  });
117
116
 
118
117
  export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
119
- id: number = getNextWindowId();
120
- ptr!: Pointer;
118
+ id = 0;
121
119
  title: string = "Electrobun";
122
120
  state: "creating" | "created" = "creating";
123
121
  url: string | null = null;
@@ -146,6 +144,10 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
146
144
  // todo (yoav): make this an array of ids or something
147
145
  webviewId!: number;
148
146
 
147
+ get ptr(): Pointer | null {
148
+ return ffi.request.getWindowPointer({ winId: this.id }) as Pointer | null;
149
+ }
150
+
149
151
  constructor(options: Partial<WindowOptionsType<T>> = defaultOptions) {
150
152
  this.title = options.title || "New Window";
151
153
  this.frame = options.frame
@@ -177,8 +179,7 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
177
179
  hidden,
178
180
  activate,
179
181
  }: Partial<WindowOptionsType<T>>) {
180
- this.ptr = ffi.request.createWindow({
181
- id: this.id,
182
+ const windowId = ffi.request.createWindow({
182
183
  title: this.title,
183
184
  url: this.url || "",
184
185
  frame: {
@@ -221,7 +222,13 @@ export class BrowserWindow<T extends RPCWithTransport = RPCWithTransport> {
221
222
  hidden: hidden ?? false,
222
223
  activate: activate ?? true,
223
224
  trafficLightOffset: this.trafficLightOffset,
224
- }) as Pointer;
225
+ });
226
+
227
+ if (!windowId) {
228
+ throw "Failed to create window";
229
+ }
230
+
231
+ this.id = windowId as number;
225
232
 
226
233
  BrowserWindowMap[this.id] = this;
227
234
 
@@ -1,3 +1,5 @@
1
+ import { readFileSync } from "fs";
2
+
1
3
  export type BuildConfigType = {
2
4
  defaultRenderer: "native" | "cef";
3
5
  availableRenderers: ("native" | "cef")[];
@@ -11,6 +13,13 @@ export type BuildConfigType = {
11
13
 
12
14
  let buildConfig: BuildConfigType | null = null;
13
15
 
16
+ function fallbackBuildConfig(): BuildConfigType {
17
+ return {
18
+ defaultRenderer: "native",
19
+ availableRenderers: ["native"],
20
+ };
21
+ }
22
+
14
23
  const BuildConfig = {
15
24
  /**
16
25
  * Get the build configuration. Loads from build.json on first call, then returns cached value.
@@ -26,10 +35,28 @@ const BuildConfig = {
26
35
  return buildConfig!;
27
36
  } catch (error) {
28
37
  // Fallback for dev mode or missing file
29
- buildConfig = {
30
- defaultRenderer: "native",
31
- availableRenderers: ["native"],
32
- };
38
+ buildConfig = fallbackBuildConfig();
39
+ return buildConfig;
40
+ }
41
+ },
42
+
43
+ /**
44
+ * Get the build configuration synchronously.
45
+ * Useful for modules that cannot use top-level await.
46
+ */
47
+ getSync: (): BuildConfigType => {
48
+ if (buildConfig) {
49
+ return buildConfig;
50
+ }
51
+
52
+ try {
53
+ const resourcesDir = "Resources";
54
+ buildConfig = JSON.parse(
55
+ readFileSync(`../${resourcesDir}/build.json`, "utf8"),
56
+ ) as BuildConfigType;
57
+ return buildConfig;
58
+ } catch (error) {
59
+ buildConfig = fallbackBuildConfig();
33
60
  return buildConfig;
34
61
  }
35
62
  },
@@ -2,7 +2,6 @@ import { ffi } from "../proc/native";
2
2
  import electrobunEventEmitter from "../events/eventEmitter";
3
3
  import { type Pointer } from "bun:ffi";
4
4
  import { WGPUView } from "./WGPUView";
5
- import { getNextWindowId } from "./windowIds";
6
5
 
7
6
 
8
7
  export type GpuWindowOptionsType = {
@@ -54,8 +53,7 @@ electrobunEventEmitter.on("close", (event: { data: { id: number } }) => {
54
53
  });
55
54
 
56
55
  export class GpuWindow {
57
- id: number = getNextWindowId();
58
- ptr!: Pointer;
56
+ id = 0;
59
57
  title: string = "Electrobun";
60
58
  state: "creating" | "created" = "creating";
61
59
  transparent: boolean = false;
@@ -73,6 +71,10 @@ export class GpuWindow {
73
71
  };
74
72
  wgpuViewId!: number;
75
73
 
74
+ get ptr(): Pointer | null {
75
+ return ffi.request.getWindowPointer({ winId: this.id }) as Pointer | null;
76
+ }
77
+
76
78
  constructor(options: Partial<GpuWindowOptionsType> = defaultOptions) {
77
79
  this.title = options.title || "New Window";
78
80
  this.frame = options.frame
@@ -93,8 +95,7 @@ export class GpuWindow {
93
95
  transparent,
94
96
  activate,
95
97
  }: Partial<GpuWindowOptionsType>) {
96
- this.ptr = ffi.request.createWindow({
97
- id: this.id,
98
+ const windowId = ffi.request.createWindow({
98
99
  title: this.title,
99
100
  url: "",
100
101
  frame: {
@@ -136,7 +137,13 @@ export class GpuWindow {
136
137
  transparent: transparent ?? false,
137
138
  activate: activate ?? true,
138
139
  trafficLightOffset: this.trafficLightOffset,
139
- }) as Pointer;
140
+ });
141
+
142
+ if (!windowId) {
143
+ throw "Failed to create window";
144
+ }
145
+
146
+ this.id = windowId as number;
140
147
 
141
148
  GpuWindowMap[this.id] = this;
142
149
 
@@ -2,14 +2,12 @@ import { ffi, type MenuItemConfig, type Rectangle } from "../proc/native";
2
2
  import electrobunEventEmitter from "../events/eventEmitter";
3
3
  import { VIEWS_FOLDER } from "./Paths";
4
4
  import { join } from "path";
5
- import { type Pointer } from "bun:ffi";
6
5
 
7
6
  type NonDividerMenuItem = Exclude<
8
7
  MenuItemConfig,
9
8
  { type: "divider" | "separator" }
10
9
  >;
11
10
 
12
- let nextTrayId = 1;
13
11
  const TrayMap: { [id: number]: Tray } = {};
14
12
 
15
13
  export type TrayOptions = {
@@ -21,9 +19,8 @@ export type TrayOptions = {
21
19
  };
22
20
 
23
21
  export class Tray {
24
- id: number = nextTrayId++;
25
- ptr: Pointer | null = null;
26
- visible = true;
22
+ id = 0;
23
+ visible = false;
27
24
  title = "";
28
25
  image = "";
29
26
  template = true;
@@ -44,36 +41,35 @@ export class Tray {
44
41
  this.width = width;
45
42
  this.height = height;
46
43
 
47
- this.createNativeTray();
48
-
49
- TrayMap[this.id] = this;
44
+ if (this.createNativeTray()) {
45
+ TrayMap[this.id] = this;
46
+ }
50
47
  }
51
48
 
52
- private createNativeTray() {
49
+ private createNativeTray(): boolean {
53
50
  try {
54
- this.ptr = ffi.request.createTray({
55
- id: this.id,
51
+ const trayId = ffi.request.createTray({
56
52
  title: this.title,
57
53
  image: this.resolveImagePath(this.image),
58
54
  template: this.template,
59
55
  width: this.width,
60
56
  height: this.height,
61
- }) as Pointer;
57
+ }) as number;
58
+
59
+ if (!trayId) {
60
+ throw new Error("Tray creation returned an invalid id");
61
+ }
62
+
63
+ this.id = trayId;
62
64
  this.visible = true;
65
+ return true;
63
66
  } catch (error) {
64
67
  console.warn("Tray creation failed:", error);
65
68
  console.warn(
66
69
  "System tray functionality may not be available on this platform",
67
70
  );
68
- this.ptr = null;
69
71
  this.visible = false;
70
- }
71
-
72
- if (this.ptr && this.menu) {
73
- ffi.request.setTrayMenu({
74
- id: this.id,
75
- menuConfig: JSON.stringify(menuConfigWithDefaults(this.menu)),
76
- });
72
+ return false;
77
73
  }
78
74
  }
79
75
 
@@ -88,13 +84,13 @@ export class Tray {
88
84
 
89
85
  setTitle(title: string) {
90
86
  this.title = title;
91
- if (!this.ptr) return;
87
+ if (!this.id) return;
92
88
  ffi.request.setTrayTitle({ id: this.id, title });
93
89
  }
94
90
 
95
91
  setImage(imgPath: string) {
96
92
  this.image = imgPath;
97
- if (!this.ptr) return;
93
+ if (!this.id) return;
98
94
  ffi.request.setTrayImage({
99
95
  id: this.id,
100
96
  image: this.resolveImagePath(imgPath),
@@ -103,7 +99,7 @@ export class Tray {
103
99
 
104
100
  setMenu(menu: Array<MenuItemConfig>) {
105
101
  this.menu = menu;
106
- if (!this.ptr) return;
102
+ if (!this.id) return;
107
103
  const menuWithDefaults = menuConfigWithDefaults(menu);
108
104
  ffi.request.setTrayMenu({
109
105
  id: this.id,
@@ -122,15 +118,19 @@ export class Tray {
122
118
  }
123
119
 
124
120
  if (!visible) {
125
- if (this.ptr) {
126
- ffi.request.removeTray({ id: this.id });
127
- this.ptr = null;
128
- }
121
+ if (this.id) ffi.request.hideTray({ id: this.id });
129
122
  this.visible = false;
130
123
  return;
131
124
  }
132
125
 
133
- this.createNativeTray();
126
+ if (!this.id) {
127
+ if (this.createNativeTray()) {
128
+ TrayMap[this.id] = this;
129
+ }
130
+ return;
131
+ }
132
+
133
+ this.visible = ffi.request.showTray({ id: this.id }) as boolean;
134
134
  }
135
135
 
136
136
  getBounds(): Rectangle {
@@ -139,12 +139,13 @@ export class Tray {
139
139
 
140
140
  remove() {
141
141
  console.log("Tray.remove() called for id:", this.id);
142
- if (this.ptr) {
143
- ffi.request.removeTray({ id: this.id });
144
- this.ptr = null;
142
+ const trayId = this.id;
143
+ if (trayId) {
144
+ ffi.request.removeTray({ id: trayId });
145
145
  }
146
146
  this.visible = false;
147
- delete TrayMap[this.id];
147
+ delete TrayMap[trayId];
148
+ this.id = 0;
148
149
  console.log("Tray removed from TrayMap");
149
150
  }
150
151