@bun-native/protocol 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
  The private native protocol shared by Bun Native packages and hosts.
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,63 +1,63 @@
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/protocol/package.json": {
11
11
  "bytes": 129,
12
12
  "sha256": "668f52c10a6db801c2d0c68c59998c489b50c0bb79cb7f60298dfbb83923b0ba"
13
13
  },
14
14
  "packages/protocol/src/app-menu.ts": {
15
- "bytes": 6435,
16
- "sha256": "52ec565f7423671a13e3bc43d49129496ed9869f22b4fa1b7e3eba79191137b8"
15
+ "bytes": 6687,
16
+ "sha256": "a0346cff0bfe26a1b2d1eaf1470521d5602d59dd54f831b3353be56499a5efc3"
17
17
  },
18
18
  "packages/protocol/src/dialogs.ts": {
19
- "bytes": 4896,
20
- "sha256": "c2ea74d7175c75710146c788b261e5566301da5952359b8e50bc69a323cdef00"
19
+ "bytes": 5261,
20
+ "sha256": "c26aab88e0e6ca9838b8c7dd471165e03f967694576a257dec88cff8343e4175"
21
21
  },
22
22
  "packages/protocol/src/drops.ts": {
23
- "bytes": 1968,
24
- "sha256": "478b9952e406853b22e32e8b5c227fdc5936363665d984c628d18581de112191"
23
+ "bytes": 2029,
24
+ "sha256": "03894f9b2b40f7645e74824de630bebc4d7d7145eafade79739248126826a42b"
25
25
  },
26
26
  "packages/protocol/src/elements.ts": {
27
- "bytes": 16161,
28
- "sha256": "3529a4f7fefa9d56880a5f32be83b7daea9ad492784478ddc413d2deb85f5283"
27
+ "bytes": 17017,
28
+ "sha256": "b831c4906806cd8b46aab29983481c85617b84b4bcc6cde515c3c2d8f5f4dfb1"
29
29
  },
30
30
  "packages/protocol/src/files.ts": {
31
- "bytes": 10163,
32
- "sha256": "d4d890f22b6abdc713e544a4e3f7b0ed53df4a1d5f44044febea8d8cf9ca96a5"
31
+ "bytes": 10818,
32
+ "sha256": "4fda4c2ebcc489f330b52ff042fe3b005b7b09718799f4b684d06c7728b86730"
33
33
  },
34
34
  "packages/protocol/src/framing.ts": {
35
- "bytes": 3259,
36
- "sha256": "47d2473785f245d8f85bd1447fba3f7ed47d5ddac170f590df5c83623064158a"
35
+ "bytes": 4321,
36
+ "sha256": "63148dfde50d4e98dafd8e1923db43d290565453a28113cf356ed3e33c4a5d70"
37
37
  },
38
38
  "packages/protocol/src/index.ts": {
39
- "bytes": 3388,
40
- "sha256": "7c7fd785b5037bc74ce0594f0ec5c49fca7ef9ae0b4dbab8fbf83d8427f6ead8"
39
+ "bytes": 2940,
40
+ "sha256": "3aa7a5bb90bbcb4c58fa999a6179fcca860b53913984efb857505d90438e79ae"
41
41
  },
42
42
  "packages/protocol/src/message-dialogs.ts": {
43
- "bytes": 3997,
44
- "sha256": "551dc23a2fde7771c28e485212d670f37db199da6f2f006344b0dc6b9064ef2b"
43
+ "bytes": 4229,
44
+ "sha256": "3fe42b3c42ff255761a867c87d0ea69f0404ee9dd64ba57cda9f9573588db210"
45
45
  },
46
46
  "packages/protocol/src/messages.ts": {
47
- "bytes": 43791,
48
- "sha256": "cef29b0d2fa4b84d73df5c9b8abbf321dafc6a66d688ad00838f0c8aa50a1d03"
47
+ "bytes": 48950,
48
+ "sha256": "17d92d1b5f08fb04f1dc9e6269291ffc7ff39f3c6ace3c9a6b7b216c6a63c2d3"
49
49
  },
50
50
  "packages/protocol/src/processes.ts": {
51
- "bytes": 9709,
52
- "sha256": "80f109623ec5f17bc21fb20c4e3093829124839c6c9e5b2457120bff0a45c778"
51
+ "bytes": 10019,
52
+ "sha256": "b64ee81b209a4e6ccf0965409c355a1bc3bfa8d813668aec385b50050be4f2b6"
53
53
  },
54
54
  "packages/protocol/src/services.ts": {
55
- "bytes": 7587,
56
- "sha256": "eb128b1ef12d2650acc53b71c2c802f35d3fb983d1f584aa1d81251adfccd37b"
55
+ "bytes": 7933,
56
+ "sha256": "4906b13929e96382fe55c8f94316e5b6629c74a6482ce25c55c621bc449a45e9"
57
57
  },
58
58
  "packages/protocol/src/values.ts": {
59
- "bytes": 2078,
60
- "sha256": "75d8f7ee9772c1aa2929a9dc4c416f42569795db089d17321398c3cdf482731b"
59
+ "bytes": 2365,
60
+ "sha256": "cca3d96f72dde820f154b6e492ea1a94ee33d8af8da44aca6fd1039912de63c1"
61
61
  },
62
62
  "LICENSE": {
63
63
  "bytes": 1075,
@@ -65,8 +65,8 @@
65
65
  }
66
66
  },
67
67
  "toolchain": {
68
- "bytes": 3012,
69
- "sha256": "00c3f707aae8abe9ca75cd2e162f5e1c7c34362a03675da39afead60d856066a"
68
+ "bytes": 3234,
69
+ "sha256": "9f4d9362cb4cc38c0f4e705295789a0cbfec46ffdd007e0541b9441a7c3003a7"
70
70
  },
71
71
  "dependencyLock": {
72
72
  "bytes": 28357,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bun-native/protocol",
3
- "version": "0.33.0",
3
+ "version": "0.33.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "files": [
package/src/app-menu.ts CHANGED
@@ -1,16 +1,15 @@
1
1
  import { ProtocolError } from './framing.ts'
2
- import { keys, object, string } from './values.ts'
2
+ import { fields, keys, object, string } from './values.ts'
3
3
 
4
4
  /** Top-level menus in one menu bar. */
5
- export const MAX_APP_MENUS = 16
5
+ const MAX_APP_MENUS = 16
6
6
  /** Items, separators and submenus in one menu bar, at every depth. */
7
7
  export const MAX_APP_MENU_ENTRIES = 128
8
8
  /** A top-level menu is depth 1; submenus nest at most two levels below it. */
9
9
  export const MAX_APP_MENU_DEPTH = 3
10
- export const MAX_APP_MENU_LABEL_BYTES = 64
11
- export const MAX_APP_MENU_ITEM_ID_BYTES = 64
10
+ const MAX_APP_MENU_LABEL_BYTES = 64
12
11
 
13
- export const APP_MENU_NAMED_KEYS = Object.freeze([
12
+ const APP_MENU_NAMED_KEYS = Object.freeze([
14
13
  'enter',
15
14
  'escape',
16
15
  'tab',
@@ -26,7 +25,7 @@ export const APP_MENU_NAMED_KEYS = Object.freeze([
26
25
  'pageup',
27
26
  'pagedown',
28
27
  ] as const)
29
- export const APP_MENU_PUNCTUATION_KEYS = Object.freeze([
28
+ const APP_MENU_PUNCTUATION_KEYS = Object.freeze([
30
29
  ',',
31
30
  '.',
32
31
  '/',
@@ -104,14 +103,17 @@ function flag(data: Record<string, unknown>, key: string): boolean {
104
103
 
105
104
  const isFunctionKey = (key: string) => /^f([1-9]|1[0-2])$/.test(key)
106
105
 
106
+ const MENU_SHORTCUT_KEYS = fields('key', 'modifiers')
107
+ const MENU_MODIFIER_KEYS = fields('control', 'alt', 'shift', 'platform')
108
+
107
109
  function parseMenuShortcut(value: unknown): MenuShortcut {
108
110
  const data = object(value)
109
- keys(data, ['key', 'modifiers'])
111
+ keys(data, MENU_SHORTCUT_KEYS)
110
112
  const key = data['key']
111
113
  if (typeof key !== 'string' || !APP_MENU_KEYS.includes(key))
112
114
  throw new ProtocolError('Menu shortcut key is not in the closed key set')
113
115
  const raw = object(data['modifiers'])
114
- keys(raw, ['control', 'alt', 'shift', 'platform'])
116
+ keys(raw, MENU_MODIFIER_KEYS)
115
117
  const modifiers = {
116
118
  control: flag(raw, 'control'),
117
119
  alt: flag(raw, 'alt'),
@@ -132,6 +134,10 @@ interface Census {
132
134
  shortcuts: Set<string>
133
135
  }
134
136
 
137
+ const ENTRIES_ITEM_KEYS = fields('kind', 'id', 'label', 'shortcut', 'enabled', 'checked')
138
+ const ENTRIES_SEPARATOR_KEYS = fields('kind')
139
+ const ENTRIES_SUBMENU_KEYS = fields('kind', 'label', 'enabled', 'items')
140
+
135
141
  function entries(value: unknown, depth: number, census: Census): AppMenuEntry[] {
136
142
  if (!Array.isArray(value) || !value.length)
137
143
  throw new ProtocolError('A menu or submenu needs at least one entry')
@@ -141,7 +147,7 @@ function entries(value: unknown, depth: number, census: Census): AppMenuEntry[]
141
147
  const data = object(raw)
142
148
  switch (data['kind']) {
143
149
  case 'item': {
144
- keys(data, ['kind', 'id', 'label', 'shortcut', 'enabled', 'checked'])
150
+ keys(data, ENTRIES_ITEM_KEYS)
145
151
  const id = parseAppMenuItemId(data['id'])
146
152
  if (census.ids.has(id))
147
153
  throw new ProtocolError('Menu item ids must be unique in a menu bar')
@@ -164,10 +170,10 @@ function entries(value: unknown, depth: number, census: Census): AppMenuEntry[]
164
170
  return entry
165
171
  }
166
172
  case 'separator':
167
- keys(data, ['kind'])
173
+ keys(data, ENTRIES_SEPARATOR_KEYS)
168
174
  return { kind: 'separator' }
169
175
  case 'submenu': {
170
- keys(data, ['kind', 'label', 'enabled', 'items'])
176
+ keys(data, ENTRIES_SUBMENU_KEYS)
171
177
  const text = label(data['label'], 'menu label')
172
178
  const enabled = flag(data, 'enabled')
173
179
  if (depth >= MAX_APP_MENU_DEPTH)
@@ -193,6 +199,8 @@ export function parseContextMenuItems(value: unknown): AppMenuEntry[] {
193
199
  return entries(value, 1, { entries: 0, ids: new Set(), shortcuts: new Set() })
194
200
  }
195
201
 
202
+ const APP_MENUS_KEYS = fields('title', 'items')
203
+
196
204
  /** An empty bar is valid: it removes the application's menus. */
197
205
  export function parseAppMenus(value: unknown): AppMenu[] {
198
206
  if (!Array.isArray(value) || value.length > MAX_APP_MENUS)
@@ -200,7 +208,7 @@ export function parseAppMenus(value: unknown): AppMenu[] {
200
208
  const census: Census = { entries: 0, ids: new Set(), shortcuts: new Set() }
201
209
  return value.map((raw) => {
202
210
  const data = object(raw)
203
- keys(data, ['title', 'items'])
211
+ keys(data, APP_MENUS_KEYS)
204
212
  return {
205
213
  title: label(data['title'], 'menu title'),
206
214
  items: entries(data['items'], 1, census),
package/src/dialogs.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ProtocolError } from './framing.ts'
2
- import { keys, object, string } from './values.ts'
2
+ import { fields, keys, object, string } from './values.ts'
3
3
 
4
4
  /** Live file dialogs per session. Each window owns at most one at a time. */
5
5
  export const MAX_FILE_DIALOGS = 16
@@ -56,9 +56,11 @@ function parseDialogPath(value: unknown, field = 'dialog path'): string {
56
56
  return path
57
57
  }
58
58
 
59
+ const FILTER_KEYS = fields('name', 'patterns')
60
+
59
61
  function filter(value: unknown): FileFilter {
60
62
  const data = object(value)
61
- keys(data, ['name', 'patterns'])
63
+ keys(data, FILTER_KEYS)
62
64
  const patterns = data['patterns']
63
65
  if (
64
66
  !Array.isArray(patterns) ||
@@ -74,9 +76,18 @@ function filter(value: unknown): FileFilter {
74
76
  }
75
77
  }
76
78
 
79
+ const FILE_DIALOG_OPTIONS_KEYS = fields(
80
+ 'mode',
81
+ 'title',
82
+ 'accept_label',
83
+ 'folder',
84
+ 'name',
85
+ 'filters'
86
+ )
87
+
77
88
  export function parseFileDialogOptions(value: unknown): FileDialogOptions {
78
89
  const data = object(value)
79
- keys(data, ['mode', 'title', 'accept_label', 'folder', 'name', 'filters'])
90
+ keys(data, FILE_DIALOG_OPTIONS_KEYS)
80
91
  const mode = FILE_DIALOG_MODES.find((candidate) => candidate === data['mode'])
81
92
  if (!mode) throw new ProtocolError('Unknown file dialog mode')
82
93
  const options: FileDialogOptions = {
@@ -104,21 +115,25 @@ export function parseFileDialogOptions(value: unknown): FileDialogOptions {
104
115
  return options
105
116
  }
106
117
 
118
+ const FILE_DIALOG_RESULT_SELECTED_KEYS = fields('kind', 'paths')
119
+ const FILE_DIALOG_RESULT_CANCELLED_KEYS = fields('kind')
120
+ const FILE_DIALOG_RESULT_FAILED_KEYS = fields('kind', 'code')
121
+
107
122
  export function parseFileDialogResult(value: unknown): FileDialogResult {
108
123
  const data = object(value)
109
124
  switch (data['kind']) {
110
125
  case 'selected': {
111
- keys(data, ['kind', 'paths'])
126
+ keys(data, FILE_DIALOG_RESULT_SELECTED_KEYS)
112
127
  const paths = data['paths']
113
128
  if (!Array.isArray(paths) || paths.length < 1 || paths.length > MAX_DIALOG_PATHS)
114
129
  throw new ProtocolError('A dialog selection holds between 1 and 256 paths')
115
130
  return { kind: 'selected', paths: paths.map((path) => parseDialogPath(path)) }
116
131
  }
117
132
  case 'cancelled':
118
- keys(data, ['kind'])
133
+ keys(data, FILE_DIALOG_RESULT_CANCELLED_KEYS)
119
134
  return { kind: 'cancelled' }
120
135
  case 'failed': {
121
- keys(data, ['kind', 'code'])
136
+ keys(data, FILE_DIALOG_RESULT_FAILED_KEYS)
122
137
  const code = (['native_failure', 'payload_too_large'] as const).find(
123
138
  (candidate) => candidate === data['code']
124
139
  )
package/src/drops.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ProtocolError } from './framing.ts'
2
- import { integer, keys, object, string } from './values.ts'
2
+ import { fields, integer, keys, object, string } from './values.ts'
3
3
 
4
4
  /** Live drop targets across a session. */
5
5
  export const MAX_DROP_TARGETS = 32
@@ -33,12 +33,14 @@ export function parseDropItemIndex(value: unknown): number {
33
33
  return integer(value, 'Dropped item index', 0, MAX_DROP_ITEMS - 1)
34
34
  }
35
35
 
36
+ const DROPPED_ITEMS_KEYS = fields('name')
37
+
36
38
  export function parseDroppedItems(value: unknown): DroppedItem[] {
37
39
  if (!Array.isArray(value) || value.length < 1 || value.length > MAX_DROP_ITEMS)
38
40
  throw new ProtocolError('A drop must deliver between 1 and ' + MAX_DROP_ITEMS + ' items')
39
41
  return value.map((entry) => {
40
42
  const data = object(entry)
41
- keys(data, ['name'])
43
+ keys(data, DROPPED_ITEMS_KEYS)
42
44
  const name = string(data['name'], 'Dropped item name', MAX_DROP_NAME_BYTES)
43
45
  if (!name || /\p{Cc}/u.test(name))
44
46
  throw new ProtocolError('Dropped item name must be nonempty without control characters')
package/src/elements.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ProtocolError } from './framing.ts'
2
- import { integer, keys, number, object, string } from './values.ts'
2
+ import { fields, integer, keys, number, object, string } from './values.ts'
3
3
 
4
4
  export interface Style {
5
5
  direction?: 'row' | 'column'
@@ -104,7 +104,7 @@ export interface AccessibleLabel {
104
104
  }
105
105
 
106
106
  /** The largest `accessible_set_size` and `accessible_position`. */
107
- export const MAX_ACCESSIBLE_SET_SIZE = 2_147_483_647
107
+ const MAX_ACCESSIBLE_SET_SIZE = 2_147_483_647
108
108
 
109
109
  /**
110
110
  * Requires `accessibility_lists_v1`. A box without `accessible_role` is layout
@@ -165,29 +165,31 @@ function requireLeafStyle(style: Style): asserts style is LeafStyle {
165
165
  throw new ProtocolError('Scrolling is supported only on box and overlay elements')
166
166
  }
167
167
 
168
+ const STYLE_KEYS = fields(
169
+ 'direction',
170
+ 'gap',
171
+ 'padding',
172
+ 'width',
173
+ 'height',
174
+ 'min_width',
175
+ 'max_width',
176
+ 'min_height',
177
+ 'max_height',
178
+ 'flex_grow',
179
+ 'flex_shrink',
180
+ 'align_items',
181
+ 'justify_content',
182
+ 'overflow',
183
+ 'scroll_anchor',
184
+ 'background',
185
+ 'color',
186
+ 'font_size',
187
+ ...DECORATION_FIELDS
188
+ )
189
+
168
190
  function parseStyle(value: unknown): Style {
169
191
  const data = object(value)
170
- keys(data, [
171
- 'direction',
172
- 'gap',
173
- 'padding',
174
- 'width',
175
- 'height',
176
- 'min_width',
177
- 'max_width',
178
- 'min_height',
179
- 'max_height',
180
- 'flex_grow',
181
- 'flex_shrink',
182
- 'align_items',
183
- 'justify_content',
184
- 'overflow',
185
- 'scroll_anchor',
186
- 'background',
187
- 'color',
188
- 'font_size',
189
- ...DECORATION_FIELDS,
190
- ])
192
+ keys(data, STYLE_KEYS)
191
193
  const style: Style = {}
192
194
  for (const key of ['gap', 'padding'] as const)
193
195
  if (key in data) style[key] = number(data[key], key, 0, 4_096)
@@ -310,24 +312,60 @@ function boxAccessibility(element: Record<string, unknown>): BoxAccessibility {
310
312
  }
311
313
  }
312
314
 
313
- /** Normalize and validate the entire property set, including JavaScript consumers. */
315
+ const ELEMENT_BOX_KEYS = fields(
316
+ 'kind',
317
+ 'accessible_role',
318
+ 'accessible_label',
319
+ 'accessible_set_size',
320
+ 'accessible_position',
321
+ 'accessible_selected',
322
+ 'style'
323
+ )
324
+ const ELEMENT_TEXT_KEYS = fields('kind', 'text', 'accessible_live', 'style')
325
+ const ELEMENT_BUTTON_KEYS = fields('kind', 'text', 'disabled', 'style')
326
+ const ELEMENT_INPUT_KEYS = fields('kind', 'value', 'placeholder', 'disabled', 'read_only', 'style')
327
+ const ELEMENT_SPLIT_KEYS = fields('kind', 'ratio', 'min_size', 'accessible_label', 'style')
328
+ const ELEMENT_OVERLAY_KEYS = fields(
329
+ 'kind',
330
+ 'anchor',
331
+ 'modal',
332
+ 'surface',
333
+ 'autofocus',
334
+ 'navigation',
335
+ 'accessible_label',
336
+ 'style'
337
+ )
338
+
339
+ /**
340
+ * Elements this module produced. Each is frozen with its style, so it is still
341
+ * exactly the normalized value that passed validation.
342
+ */
343
+ const parsed = new WeakSet<Element>()
344
+
345
+ /**
346
+ * Normalize and validate the entire property set, including JavaScript consumers.
347
+ *
348
+ * The result is frozen and returned unchanged when parsed again, so core's
349
+ * normalized elements cross the runtime boundary in O(1). Any other value,
350
+ * including an unfrozen copy of a parsed element, is validated in full.
351
+ */
314
352
  export function parseElement(value: unknown): Element {
353
+ if (parsed.has(value as Element)) return value as Element
354
+ const element = validElement(value)
355
+ Object.freeze(element.style)
356
+ parsed.add(Object.freeze(element))
357
+ return element
358
+ }
359
+
360
+ function validElement(value: unknown): Element {
315
361
  const element = object(value)
316
362
  const style = 'style' in element ? parseStyle(element['style']) : {}
317
363
  switch (element['kind']) {
318
364
  case 'box':
319
- keys(element, [
320
- 'kind',
321
- 'accessible_role',
322
- 'accessible_label',
323
- 'accessible_set_size',
324
- 'accessible_position',
325
- 'accessible_selected',
326
- 'style',
327
- ])
365
+ keys(element, ELEMENT_BOX_KEYS)
328
366
  return { kind: 'box', ...boxAccessibility(element), style }
329
367
  case 'text':
330
- keys(element, ['kind', 'text', 'accessible_live', 'style'])
368
+ keys(element, ELEMENT_TEXT_KEYS)
331
369
  requireLeafStyle(style)
332
370
  return {
333
371
  kind: 'text',
@@ -343,7 +381,7 @@ export function parseElement(value: unknown): Element {
343
381
  style,
344
382
  }
345
383
  case 'button': {
346
- keys(element, ['kind', 'text', 'disabled', 'style'])
384
+ keys(element, ELEMENT_BUTTON_KEYS)
347
385
  requireLeafStyle(style)
348
386
  if ('disabled' in element && typeof element['disabled'] !== 'boolean')
349
387
  throw new ProtocolError('disabled must be a boolean')
@@ -355,7 +393,7 @@ export function parseElement(value: unknown): Element {
355
393
  }
356
394
  }
357
395
  case 'input': {
358
- keys(element, ['kind', 'value', 'placeholder', 'disabled', 'read_only', 'style'])
396
+ keys(element, ELEMENT_INPUT_KEYS)
359
397
  requireLeafStyle(style)
360
398
  for (const name of ['disabled', 'read_only'])
361
399
  if (name in element && typeof element[name] !== 'boolean')
@@ -371,7 +409,7 @@ export function parseElement(value: unknown): Element {
371
409
  }
372
410
  }
373
411
  case 'split': {
374
- keys(element, ['kind', 'ratio', 'min_size', 'accessible_label', 'style'])
412
+ keys(element, ELEMENT_SPLIT_KEYS)
375
413
  requireLeafStyle(style)
376
414
  if (
377
415
  style.gap !== undefined ||
@@ -388,16 +426,7 @@ export function parseElement(value: unknown): Element {
388
426
  }
389
427
  }
390
428
  case 'overlay': {
391
- keys(element, [
392
- 'kind',
393
- 'anchor',
394
- 'modal',
395
- 'surface',
396
- 'autofocus',
397
- 'navigation',
398
- 'accessible_label',
399
- 'style',
400
- ])
429
+ keys(element, ELEMENT_OVERLAY_KEYS)
401
430
  const label = accessibleLabel(element)
402
431
  const anchor =
403
432
  'anchor' in element ? choice(element['anchor'], 'anchor', ['window', 'parent']) : 'window'
package/src/files.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { ProtocolError } from './framing.ts'
2
- import { integer, keys, object, string } from './values.ts'
2
+ import { fields, integer, keys, object, string } from './values.ts'
3
3
 
4
4
  export const MAX_FILESYSTEM_ROOTS = 8
5
5
  export const MAX_FILE_GRANTS = 32
@@ -87,9 +87,11 @@ export function parseFileAccess(value: unknown): FileAccess {
87
87
  return value as FileAccess
88
88
  }
89
89
 
90
+ const ROOT_KEYS = fields('id', 'path', 'access')
91
+
90
92
  function root(value: unknown): FilesystemRoot {
91
93
  const data = object(value)
92
- keys(data, ['id', 'path', 'access'])
94
+ keys(data, ROOT_KEYS)
93
95
  return {
94
96
  id: parseRootId(data['id']),
95
97
  path: parseRootPath(data['path']),
@@ -158,6 +160,11 @@ export function parseChunk(value: unknown): string {
158
160
  const WRITE_MODES: readonly WriteMode[] = ['create', 'truncate', 'append']
159
161
  const FILE_KINDS: readonly FileKind[] = ['file', 'directory', 'symlink', 'other']
160
162
 
163
+ const FILE_OPERATION_STAT_KEYS = fields('kind')
164
+ const FILE_OPERATION_LIST_KEYS = fields('kind', 'after')
165
+ const FILE_OPERATION_READ_KEYS = fields('kind', 'offset', 'length')
166
+ const FILE_OPERATION_WRITE_KEYS = fields('kind', 'mode', 'data')
167
+
161
168
  export function parseFileOperation(value: unknown, path: string): FileOperation {
162
169
  const data = object(value)
163
170
  const writes = ['write', 'create_directory', 'remove'].includes(data['kind'] as string)
@@ -167,15 +174,15 @@ export function parseFileOperation(value: unknown, path: string): FileOperation
167
174
  case 'stat':
168
175
  case 'create_directory':
169
176
  case 'remove':
170
- keys(data, ['kind'])
177
+ keys(data, FILE_OPERATION_STAT_KEYS)
171
178
  return { kind: data['kind'] }
172
179
  case 'list':
173
- keys(data, ['kind', 'after'])
180
+ keys(data, FILE_OPERATION_LIST_KEYS)
174
181
  return 'after' in data
175
182
  ? { kind: 'list', after: parseFileName(data['after']) }
176
183
  : { kind: 'list' }
177
184
  case 'read':
178
- keys(data, ['kind', 'offset', 'length'])
185
+ keys(data, FILE_OPERATION_READ_KEYS)
179
186
  return {
180
187
  kind: 'read',
181
188
  offset: integer(
@@ -187,7 +194,7 @@ export function parseFileOperation(value: unknown, path: string): FileOperation
187
194
  length: integer(data['length'], 'file read length', 1, MAX_FILE_CHUNK_BYTES),
188
195
  }
189
196
  case 'write': {
190
- keys(data, ['kind', 'mode', 'data'])
197
+ keys(data, FILE_OPERATION_WRITE_KEYS)
191
198
  const mode = WRITE_MODES.find((item) => item === data['mode'])
192
199
  if (!mode) throw new ProtocolError('Unknown file write mode')
193
200
  return { kind: 'write', mode, data: parseChunk(data['data']) }
@@ -211,11 +218,18 @@ function compareBytes(left: string, right: string): number {
211
218
  return a.length - b.length
212
219
  }
213
220
 
221
+ const FILE_RESULT_STAT_KEYS = fields('kind', 'file_kind', 'size', 'modified_ms')
222
+ const FILE_RESULT_LIST_KEYS = fields('kind', 'entries', 'complete', 'omitted')
223
+ const FILE_LIST_ENTRY_KEYS = fields('name', 'kind')
224
+ const FILE_RESULT_READ_KEYS = fields('kind', 'data', 'end_of_file')
225
+ const FILE_RESULT_WRITTEN_KEYS = fields('kind', 'bytes')
226
+ const FILE_RESULT_DONE_KEYS = fields('kind')
227
+
214
228
  export function parseFileResult(value: unknown): FileResult {
215
229
  const data = object(value)
216
230
  switch (data['kind']) {
217
231
  case 'stat':
218
- keys(data, ['kind', 'file_kind', 'size', 'modified_ms'])
232
+ keys(data, FILE_RESULT_STAT_KEYS)
219
233
  return {
220
234
  kind: 'stat',
221
235
  file_kind: fileKind(data['file_kind']),
@@ -227,13 +241,13 @@ export function parseFileResult(value: unknown): FileResult {
227
241
  ),
228
242
  }
229
243
  case 'list': {
230
- keys(data, ['kind', 'entries', 'complete', 'omitted'])
244
+ keys(data, FILE_RESULT_LIST_KEYS)
231
245
  const entries = data['entries']
232
246
  if (!Array.isArray(entries) || entries.length > MAX_DIRECTORY_ENTRIES)
233
247
  throw new ProtocolError('Directory page exceeds ' + MAX_DIRECTORY_ENTRIES + ' entries')
234
248
  const parsed = entries.map((entry) => {
235
249
  const item = object(entry)
236
- keys(item, ['name', 'kind'])
250
+ keys(item, FILE_LIST_ENTRY_KEYS)
237
251
  return { name: parseFileName(item['name']), kind: fileKind(item['kind']) }
238
252
  })
239
253
  for (let index = 1; index < parsed.length; index++)
@@ -249,18 +263,18 @@ export function parseFileResult(value: unknown): FileResult {
249
263
  }
250
264
  }
251
265
  case 'read':
252
- keys(data, ['kind', 'data', 'end_of_file'])
266
+ keys(data, FILE_RESULT_READ_KEYS)
253
267
  if (typeof data['end_of_file'] !== 'boolean')
254
268
  throw new ProtocolError('File end-of-file flag must be a boolean')
255
269
  return { kind: 'read', data: parseChunk(data['data']), end_of_file: data['end_of_file'] }
256
270
  case 'written':
257
- keys(data, ['kind', 'bytes'])
271
+ keys(data, FILE_RESULT_WRITTEN_KEYS)
258
272
  return {
259
273
  kind: 'written',
260
274
  bytes: integer(data['bytes'], 'written bytes', 0, MAX_FILE_CHUNK_BYTES),
261
275
  }
262
276
  case 'done':
263
- keys(data, ['kind'])
277
+ keys(data, FILE_RESULT_DONE_KEYS)
264
278
  return { kind: 'done' }
265
279
  default:
266
280
  throw new ProtocolError('Unknown file result')