@bun-native/runtime 0.33.0 → 0.33.1

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
@@ -2,4 +2,4 @@
2
2
 
3
3
  Bun Native host process lifecycle and bridge runtime.
4
4
 
5
- Part of [Bun Native](https://github.com/0xPlayerOne/bun-native), an experimental developer preview. Version 0.33.0 was built from [fa8bef754122](https://github.com/0xPlayerOne/bun-native/tree/fa8bef754122621d4235eb06e30bb1085b4f7b1e) and published from GitHub Actions with npm provenance. Use the Bun revision pinned in that commit's toolchain.json. Start with the [getting-started guide](https://github.com/0xPlayerOne/bun-native/blob/fa8bef754122621d4235eb06e30bb1085b4f7b1e/docs/getting-started.md); `bun-native host install` fetches the matching release host. The license notice is in LICENSE.
5
+ Part of [Bun Native](https://github.com/0xPlayerOne/bun-native), an experimental developer preview. Version 0.33.1 was built from [c56fdd675365](https://github.com/0xPlayerOne/bun-native/tree/c56fdd6753653a10144a4167c1d3dfb3c38a0647) and published from GitHub Actions with npm provenance. Use the Bun revision pinned in that commit's toolchain.json. Start with the [getting-started guide](https://github.com/0xPlayerOne/bun-native/blob/c56fdd6753653a10144a4167c1d3dfb3c38a0647/docs/getting-started.md); `bun-native host install` fetches the matching release host. The license notice is in LICENSE.
@@ -1,31 +1,31 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "commit": "fa8bef754122621d4235eb06e30bb1085b4f7b1e",
4
- "tree": "f805e2eb19a4e10fc29d1dc827cf5bc617b5773b",
3
+ "commit": "c56fdd6753653a10144a4167c1d3dfb3c38a0647",
4
+ "tree": "c6e81b3f9d041e11dd0413b5fdd7a50910383854",
5
5
  "files": {
6
6
  "package.json": {
7
- "bytes": 2164,
8
- "sha256": "db50b6f81f25c113a46f8c9b038bc548a92effaeb4ad9562e5e95044baa7b66f"
7
+ "bytes": 2115,
8
+ "sha256": "caf866d56f3cd7530d5735ecbf62512e9e22526bc44c75a4b48a18c26352c0b8"
9
9
  },
10
10
  "packages/runtime/package.json": {
11
11
  "bytes": 195,
12
12
  "sha256": "30d8e6684ee94e266b3bc63d151f718a5d0a8804b2155c11f74de3e7189af63a"
13
13
  },
14
14
  "packages/runtime/src/exit-diagnostic.ts": {
15
- "bytes": 4876,
16
- "sha256": "f73b2259d80a6584427ad3b27ad239b5950b816468cca8d64bdfc35a5f3cfce8"
15
+ "bytes": 5059,
16
+ "sha256": "0c93ad077ad4cfcdf32a8092da1aff6cdc0f65322feadf538768439d67b11e06"
17
17
  },
18
18
  "packages/runtime/src/index.ts": {
19
- "bytes": 38743,
20
- "sha256": "aba3c6c58e2b9089b4faaf32e512736e660314bc2c0f74f62fe6dba45df5b053"
19
+ "bytes": 39579,
20
+ "sha256": "a5f20057a2baa2117176a59c29e65438904f3758bfed3e29e981b55758e93adb"
21
21
  },
22
22
  "packages/runtime/src/installation.ts": {
23
23
  "bytes": 1871,
24
24
  "sha256": "a89c677fad56ace6839bda86891fafa088f3a37e0248c313a72d7918a0538a10"
25
25
  },
26
26
  "packages/runtime/src/startup-trace.ts": {
27
- "bytes": 3021,
28
- "sha256": "b34971a20cb541fa8cd97eb611d59923297844186b6653ecb607b4b29bf80b2d"
27
+ "bytes": 3345,
28
+ "sha256": "19efe954374e1cd486277bc8df144d8dd711cffd12e2888d14021c871de840d6"
29
29
  },
30
30
  "LICENSE": {
31
31
  "bytes": 1075,
@@ -33,8 +33,8 @@
33
33
  }
34
34
  },
35
35
  "toolchain": {
36
- "bytes": 3012,
37
- "sha256": "00c3f707aae8abe9ca75cd2e162f5e1c7c34362a03675da39afead60d856066a"
36
+ "bytes": 3234,
37
+ "sha256": "9f4d9362cb4cc38c0f4e705295789a0cbfec46ffdd007e0541b9441a7c3003a7"
38
38
  },
39
39
  "dependencyLock": {
40
40
  "bytes": 28357,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bun-native/runtime",
3
- "version": "0.33.0",
3
+ "version": "0.33.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -15,7 +15,7 @@
15
15
  "exports": "./src/index.ts",
16
16
  "description": "Bun Native host process lifecycle and bridge runtime.",
17
17
  "dependencies": {
18
- "@bun-native/protocol": "0.33.0"
18
+ "@bun-native/protocol": "0.33.1"
19
19
  },
20
20
  "repository": {
21
21
  "type": "git",
@@ -46,10 +46,7 @@ function escapeRegExp(text: string): string {
46
46
  * Removes inherited environment values, assignments and credential-shaped
47
47
  * tokens from native output. Over-redaction is acceptable; leaks are not.
48
48
  */
49
- function redactDiagnostic(
50
- text: string,
51
- environment: Readonly<Record<string, string | undefined>>
52
- ): string {
49
+ function redact(text: string, environment: Readonly<Record<string, string | undefined>>): string {
53
50
  let redacted = text.replace(/\p{Cc}/gu, (control) =>
54
51
  control === '\n' || control === '\t' ? control : ''
55
52
  )
@@ -72,6 +69,22 @@ function redactDiagnostic(
72
69
  .replace(/\b[A-Za-z0-9+_-]{32,}={0,2}/g, REDACTED)
73
70
  }
74
71
 
72
+ /**
73
+ * Captured native stderr, safe to log or record. A clipped first line can hold
74
+ * the end of a secret that no longer matches its full environment value, so it
75
+ * is dropped before redaction.
76
+ */
77
+ export function redactDiagnostic(
78
+ stderr: string,
79
+ clipped: boolean,
80
+ environment: Readonly<Record<string, string | undefined>>
81
+ ): string {
82
+ return redact(
83
+ clipped ? stderr.slice(stderr.indexOf('\n') + 1 || stderr.length) : stderr,
84
+ environment
85
+ )
86
+ }
87
+
75
88
  export function exitDiagnostic(input: {
76
89
  exitCode: number | null
77
90
  signal: string | null
@@ -88,17 +101,12 @@ export function exitDiagnostic(input: {
88
101
  panicReport?: string | null
89
102
  environment: Readonly<Record<string, string | undefined>>
90
103
  }): Readonly<NativeHostExitDiagnostic> {
91
- // A clipped first line can hold the end of a secret that no longer matches
92
- // its full environment value, so it is dropped before redaction.
93
- const stderr = input.stderrClipped
94
- ? input.stderr.slice(input.stderr.indexOf('\n') + 1 || input.stderr.length)
95
- : input.stderr
96
- const redacted = redactDiagnostic(stderr, input.environment)
104
+ const redacted = redactDiagnostic(input.stderr, input.stderrClipped, input.environment)
97
105
  let panic = PANIC_LOCATION.exec(redacted)
98
106
  // The kept report leads the tail only when the captured stderr lost it.
99
107
  const report =
100
108
  !panic && input.panicReport
101
- ? redactDiagnostic(input.panicReport, input.environment)
109
+ ? redact(input.panicReport, input.environment)
102
110
  .trimEnd()
103
111
  .slice(0, MAX_DIAGNOSTIC_TAIL / 2)
104
112
  : null
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  processArguments,
7
7
  encodeFrame,
8
8
  FrameDecoder,
9
+ MAX_EVENT_FRAME_BYTES,
9
10
  parseCommand,
10
11
  parseEvent,
11
12
  PROTOCOL_VERSION,
@@ -18,6 +19,7 @@ import {
18
19
  type ServicePolicy,
19
20
  type Capability,
20
21
  type FileAccess,
22
+ type HostEvent,
21
23
  } from '@bun-native/protocol'
22
24
  import { isAbsolute } from 'node:path'
23
25
  import type { ReadableStreamDefaultReader } from 'node:stream/web'
@@ -31,6 +33,7 @@ export {
31
33
  type NativeHostExitReason,
32
34
  } from './exit-diagnostic.ts'
33
35
  export { installedRelease, reportHealthy, type InstalledRelease } from './installation.ts'
36
+ export type { ApplicationEvent, HostEvent, WindowEvent } from '@bun-native/protocol'
34
37
 
35
38
  type WithoutId<T> = T extends { id: number } ? Omit<T, 'id'> : never
36
39
  export type WindowCommand = WithoutId<
@@ -71,10 +74,6 @@ export type WindowCommand = WithoutId<
71
74
  }
72
75
  >
73
76
  >
74
- export type WindowEvent = Extract<NativeEvent, { sequence: number; window_id: number }>
75
- /** Sequenced events owned by the application rather than one window. */
76
- export type ApplicationEvent = Extract<NativeEvent, { type: 'app_menu_item' }>
77
- export type HostEvent = WindowEvent | ApplicationEvent
78
77
  export type HostResponse = Extract<
79
78
  NativeEvent,
80
79
  {
@@ -351,6 +350,8 @@ export class NativeHost implements AsyncIterable<HostEvent> {
351
350
  private readonly events: HostEvent[] = []
352
351
  private readonly finished: Promise<void>
353
352
  private waiting: WaitingReader | undefined
353
+ /** Queued events the consumer has taken; progress while reading is paused. */
354
+ private taken = 0
354
355
  private failure: Error | undefined
355
356
  private phase: 'starting' | 'running' | 'stopping' | 'closed' = 'starting'
356
357
  private ready = false
@@ -428,6 +429,11 @@ export class NativeHost implements AsyncIterable<HostEvent> {
428
429
  }
429
430
  }
430
431
 
432
+ /** Whether the transport is still open (false once it failed or shut down). */
433
+ get running(): boolean {
434
+ return this.phase === 'running'
435
+ }
436
+
431
437
  get limits(): Readonly<Limits> {
432
438
  if (!this.negotiatedLimits) throw new ProtocolError('Native limits are not negotiated')
433
439
  return this.negotiatedLimits
@@ -574,7 +580,10 @@ export class NativeHost implements AsyncIterable<HostEvent> {
574
580
  next: () => {
575
581
  if (this.failure) return Promise.reject(this.failure)
576
582
  const event = this.events.shift()
577
- if (event) return Promise.resolve({ done: false, value: event })
583
+ if (event) {
584
+ this.taken += 1
585
+ return Promise.resolve({ done: false, value: event })
586
+ }
578
587
  if (this.phase === 'closed') return Promise.resolve({ done: true, value: undefined })
579
588
  if (this.waiting)
580
589
  return Promise.reject(new Error('Concurrent event reads are not supported'))
@@ -660,12 +669,13 @@ export class NativeHost implements AsyncIterable<HostEvent> {
660
669
  }
661
670
 
662
671
  private async readStdout(): Promise<void> {
663
- const decoder = new FrameDecoder()
672
+ const decoder = new FrameDecoder(MAX_EVENT_FRAME_BYTES)
664
673
  try {
665
674
  for (;;) {
666
675
  const { done, value } = await this.stdout.read()
667
676
  if (done) break
668
677
  for (const raw of decoder.push(value)) this.receive(parseEvent(raw))
678
+ if (this.events.length >= MAX_QUEUED_EVENTS) await this.drainEvents()
669
679
  }
670
680
  decoder.finish()
671
681
  if (
@@ -679,6 +689,21 @@ export class NativeHost implements AsyncIterable<HostEvent> {
679
689
  }
680
690
  }
681
691
 
692
+ /**
693
+ * One pipe read can coalesce more frames than the cap while the consumer is
694
+ * keeping up. Over the cap, reading pauses for macrotask turns while the
695
+ * consumer takes events, and fails only a turn in which it took none, so
696
+ * queued events stay bounded by the cap plus one read.
697
+ */
698
+ private async drainEvents(): Promise<void> {
699
+ while (this.events.length >= MAX_QUEUED_EVENTS) {
700
+ const taken = this.taken
701
+ await new Promise<void>((resolve) => setImmediate(resolve))
702
+ if (this.phase === 'closed') return
703
+ if (this.taken === taken) throw new ProtocolError('Native event limit exceeded')
704
+ }
705
+ }
706
+
682
707
  private async readStderr(): Promise<void> {
683
708
  const decoder = new TextDecoder()
684
709
  try {
@@ -771,7 +796,12 @@ export class NativeHost implements AsyncIterable<HostEvent> {
771
796
  clearTimeout(drainDeadline)
772
797
  this.cancelOutput()
773
798
  await Promise.allSettled(streams)
774
- this.startupTrace?.finished(this.process.exitCode, this.stderrTail, this.process.pid)
799
+ this.startupTrace?.finished(
800
+ this.process.exitCode,
801
+ this.stderrTail,
802
+ this.stderrClipped,
803
+ this.process.pid
804
+ )
775
805
  }
776
806
  }
777
807
 
@@ -864,8 +894,6 @@ export class NativeHost implements AsyncIterable<HostEvent> {
864
894
  this.waiting.resolve({ done: false, value: event })
865
895
  this.waiting = undefined
866
896
  } else {
867
- if (this.events.length >= MAX_QUEUED_EVENTS)
868
- throw new ProtocolError('Native event limit exceeded')
869
897
  this.events.push(event)
870
898
  }
871
899
  }
@@ -1016,7 +1044,7 @@ export class NativeHost implements AsyncIterable<HostEvent> {
1016
1044
  private fail(cause: unknown): void {
1017
1045
  if (this.failure) return
1018
1046
  this.failure = cause instanceof Error ? cause : new Error(String(cause))
1019
- this.startupTrace?.failure(this.failure, this.stderrTail, this.process.pid)
1047
+ this.startupTrace?.failure(this.failure, this.stderrTail, this.stderrClipped, this.process.pid)
1020
1048
  this.phase = 'closed'
1021
1049
  for (const request of this.pending.values()) {
1022
1050
  clearTimeout(request.timer)
@@ -1,6 +1,7 @@
1
1
  import type { Command, NativeEvent } from '@bun-native/protocol'
2
2
  import { appendFileSync } from 'node:fs'
3
3
  import { isAbsolute } from 'node:path'
4
+ import { redactDiagnostic } from './exit-diagnostic.ts'
4
5
 
5
6
  const MAX_RECORDS = 256
6
7
 
@@ -51,18 +52,27 @@ export class StartupTrace {
51
52
  })
52
53
  }
53
54
 
54
- failure(error: Error, stderr: string, hostPid: number): void {
55
+ /** `stderr` is the bounded capture; it is recorded redacted, like exit diagnostics. */
56
+ failure(error: Error, stderr: string, clipped: boolean, hostPid: number): void {
55
57
  this.write(
56
58
  'transport_failed',
57
- { host_pid: hostPid, error: error.message.slice(-1_024), stderr_tail: stderr },
59
+ {
60
+ host_pid: hostPid,
61
+ error: error.message.slice(-1_024),
62
+ stderr_tail: redactDiagnostic(stderr, clipped, process.env),
63
+ },
58
64
  true
59
65
  )
60
66
  }
61
67
 
62
- finished(exitCode: number | null, stderr: string, hostPid: number): void {
68
+ finished(exitCode: number | null, stderr: string, clipped: boolean, hostPid: number): void {
63
69
  this.write(
64
70
  'transport_finished',
65
- { host_pid: hostPid, exit_code: exitCode, stderr_tail: stderr },
71
+ {
72
+ host_pid: hostPid,
73
+ exit_code: exitCode,
74
+ stderr_tail: redactDiagnostic(stderr, clipped, process.env),
75
+ },
66
76
  true
67
77
  )
68
78
  }