@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/src/messages.ts CHANGED
@@ -75,6 +75,7 @@ import {
75
75
  type DropRejection,
76
76
  } from './drops.ts'
77
77
  import {
78
+ fields,
78
79
  id,
79
80
  integer,
80
81
  keys,
@@ -85,8 +86,9 @@ import {
85
86
  string,
86
87
  } from './values.ts'
87
88
 
88
- export const PROTOCOL_VERSION = 11
89
- export const MAX_OPERATIONS = 256
89
+ export const PROTOCOL_VERSION = 12
90
+ /** Four operations per node of a full window: a whole-window replacement fits one batch. */
91
+ export const MAX_OPERATIONS = 16_384
90
92
  const MAX_NODES_PER_WINDOW = 4_096
91
93
  const MAX_WINDOWS = 16
92
94
  const MAX_DEPTH = 64
@@ -614,24 +616,41 @@ export type NativeEvent =
614
616
  /** Application-scoped: a menu bar belongs to the application, not a window. */
615
617
  | { type: 'app_menu_item'; sequence: number; menu_id: number; item_id: string }
616
618
  | { type: 'error'; id?: number; code: ErrorCode; message: string }
619
+ /** Sequenced events owned by one window. */
620
+ export type WindowEvent = Extract<NativeEvent, { sequence: number; window_id: number }>
621
+ /** Sequenced events owned by the application rather than one window. */
622
+ export type ApplicationEvent = Extract<NativeEvent, { type: 'app_menu_item' }>
623
+ /** Unsolicited events, as opposed to responses that carry a request id. */
624
+ export type HostEvent = WindowEvent | ApplicationEvent
617
625
 
618
626
  function version(value: unknown): typeof PROTOCOL_VERSION {
619
627
  if (value !== PROTOCOL_VERSION) throw new ProtocolError('Native protocol version mismatch')
620
628
  return PROTOCOL_VERSION
621
629
  }
622
630
 
631
+ /** Property names of a window-scoped command, reply or unsolicited event. */
632
+ const windowCommand = (...names: string[]) => fields('id', 'type', 'window_id', ...names)
633
+ const windowReply = (...names: string[]) => fields('type', 'id', 'window_id', ...names)
634
+ const windowNotice = (...names: string[]) => fields('type', 'sequence', 'window_id', ...names)
635
+
636
+ const OPERATION_CREATE_KEYS = fields('type', 'node_id', 'element')
637
+ const OPERATION_SET_KEYS = fields('type', 'node_id', 'element', 'expected_input_revision')
638
+ const OPERATION_INSERT_KEYS = fields('type', 'parent_id', 'child_id', 'index')
639
+ const OPERATION_DELETE_KEYS = fields('type', 'node_id')
640
+ const OPERATION_SCROLL_TO_KEYS = fields('type', 'node_id', 'x', 'y')
641
+
623
642
  function operation(value: unknown): Operation {
624
643
  const data = object(value)
625
644
  switch (data['type']) {
626
645
  case 'create':
627
- keys(data, ['type', 'node_id', 'element'])
646
+ keys(data, OPERATION_CREATE_KEYS)
628
647
  return {
629
648
  type: 'create',
630
649
  node_id: id(data['node_id']),
631
650
  element: parseElement(data['element']),
632
651
  }
633
652
  case 'set': {
634
- keys(data, ['type', 'node_id', 'element', 'expected_input_revision'])
653
+ keys(data, OPERATION_SET_KEYS)
635
654
  const node_id = id(data['node_id'])
636
655
  const element = parseElement(data['element'])
637
656
  if (element.kind === 'input')
@@ -646,7 +665,7 @@ function operation(value: unknown): Operation {
646
665
  return { type: 'set', node_id, element }
647
666
  }
648
667
  case 'insert':
649
- keys(data, ['type', 'parent_id', 'child_id', 'index'])
668
+ keys(data, OPERATION_INSERT_KEYS)
650
669
  return {
651
670
  type: 'insert',
652
671
  parent_id: id(data['parent_id']),
@@ -656,10 +675,10 @@ function operation(value: unknown): Operation {
656
675
  case 'delete':
657
676
  case 'focus':
658
677
  case 'scroll_into_view':
659
- keys(data, ['type', 'node_id'])
678
+ keys(data, OPERATION_DELETE_KEYS)
660
679
  return { type: data['type'], node_id: id(data['node_id']) }
661
680
  case 'scroll_to': {
662
- keys(data, ['type', 'node_id', 'x', 'y'])
681
+ keys(data, OPERATION_SCROLL_TO_KEYS)
663
682
  if (!('x' in data) && !('y' in data)) throw new ProtocolError('scroll_to requires x or y')
664
683
  const offset = (axis: 'x' | 'y') =>
665
684
  axis in data ? { [axis]: number(data[axis], 'scroll ' + axis, 0, MAX_SCROLL_OFFSET) } : {}
@@ -670,20 +689,50 @@ function operation(value: unknown): Operation {
670
689
  }
671
690
  }
672
691
 
692
+ const COMMAND_HELLO_KEYS = fields('id', 'type', 'protocol')
693
+ const COMMAND_OPEN_KEYS = windowCommand('root_id', 'title', 'width', 'height')
694
+ const COMMAND_BATCH_KEYS = windowCommand('base_revision', 'operations')
695
+ const COMMAND_CLOSE_KEYS = windowCommand()
696
+ const COMMAND_CLIPBOARD_WRITE_KEYS = windowCommand('text')
697
+ const COMMAND_REGISTER_GLOBAL_SHORTCUT_KEYS = windowCommand('shortcut_id', 'key', 'modifiers')
698
+ const COMMAND_UNREGISTER_GLOBAL_SHORTCUT_KEYS = windowCommand('shortcut_id')
699
+ const COMMAND_POST_NOTIFICATION_KEYS = windowCommand('notification_id', 'title', 'body')
700
+ const COMMAND_WITHDRAW_NOTIFICATION_KEYS = windowCommand('notification_id')
701
+ const COMMAND_OPEN_FILE_GRANT_KEYS = windowCommand('grant_id', 'root', 'access')
702
+ const COMMAND_CLOSE_FILE_GRANT_KEYS = windowCommand('grant_id')
703
+ const COMMAND_FILE_REQUEST_KEYS = windowCommand('grant_id', 'path', 'operation')
704
+ const COMMAND_SPAWN_PROCESS_KEYS = windowCommand('process_id', 'launch')
705
+ const COMMAND_WRITE_PROCESS_KEYS = windowCommand('process_id', 'data')
706
+ const COMMAND_CLOSE_PROCESS_INPUT_KEYS = windowCommand('process_id')
707
+ const COMMAND_RESIZE_PROCESS_KEYS = windowCommand('process_id', 'size')
708
+ const COMMAND_SIGNAL_PROCESS_KEYS = windowCommand('process_id', 'signal')
709
+ const COMMAND_ACKNOWLEDGE_PROCESS_OUTPUT_KEYS = windowCommand('process_id', 'chunk')
710
+ const COMMAND_SHOW_FILE_DIALOG_KEYS = windowCommand('dialog_id', 'options')
711
+ const COMMAND_CLOSE_FILE_DIALOG_KEYS = windowCommand('dialog_id')
712
+ const COMMAND_SHOW_MESSAGE_DIALOG_KEYS = windowCommand('dialog_id', 'options')
713
+ const COMMAND_CLOSE_MESSAGE_DIALOG_KEYS = windowCommand('dialog_id')
714
+ const COMMAND_SHOW_CONTEXT_MENU_KEYS = windowCommand('menu_id', 'x', 'y', 'items')
715
+ const COMMAND_CLOSE_CONTEXT_MENU_KEYS = windowCommand('menu_id')
716
+ const COMMAND_REGISTER_DROP_TARGET_KEYS = windowCommand('target_id', 'node_id')
717
+ const COMMAND_UNREGISTER_DROP_TARGET_KEYS = windowCommand('target_id')
718
+ const COMMAND_RESOLVE_DROPPED_ITEM_KEYS = windowCommand('target_id', 'drop_id', 'item', 'root')
719
+ const COMMAND_SET_APP_MENU_KEYS = fields('id', 'type', 'menu_id', 'menus')
720
+ const COMMAND_SHUTDOWN_KEYS = fields('id', 'type')
721
+
673
722
  /** Validate before crossing the native boundary, including JavaScript callers. */
674
723
  export function parseCommand(value: unknown): Command {
675
724
  const message = object(value)
676
725
  const request = id(message['id'])
677
726
  switch (message['type']) {
678
727
  case 'hello':
679
- keys(message, ['id', 'type', 'protocol'])
728
+ keys(message, COMMAND_HELLO_KEYS)
680
729
  return {
681
730
  id: request,
682
731
  type: 'hello',
683
732
  protocol: version(message['protocol']),
684
733
  }
685
734
  case 'open': {
686
- keys(message, ['id', 'type', 'window_id', 'root_id', 'title', 'width', 'height'])
735
+ keys(message, COMMAND_OPEN_KEYS)
687
736
  const title = string(message['title'], 'title', MAX_TITLE_BYTES)
688
737
  if (!title || /\p{Cc}/u.test(title))
689
738
  throw new ProtocolError('Window title must be nonempty and contain no control characters')
@@ -698,7 +747,7 @@ export function parseCommand(value: unknown): Command {
698
747
  }
699
748
  }
700
749
  case 'batch': {
701
- keys(message, ['id', 'type', 'window_id', 'base_revision', 'operations'])
750
+ keys(message, COMMAND_BATCH_KEYS)
702
751
  const operations = message['operations']
703
752
  if (!Array.isArray(operations) || operations.length < 1 || operations.length > MAX_OPERATIONS)
704
753
  throw new ProtocolError(
@@ -715,14 +764,14 @@ export function parseCommand(value: unknown): Command {
715
764
  case 'close':
716
765
  case 'display_snapshot':
717
766
  case 'clipboard_read':
718
- keys(message, ['id', 'type', 'window_id'])
767
+ keys(message, COMMAND_CLOSE_KEYS)
719
768
  return {
720
769
  id: request,
721
770
  type: message['type'],
722
771
  window_id: id(message['window_id']),
723
772
  }
724
773
  case 'clipboard_write':
725
- keys(message, ['id', 'type', 'window_id', 'text'])
774
+ keys(message, COMMAND_CLIPBOARD_WRITE_KEYS)
726
775
  return {
727
776
  id: request,
728
777
  type: 'clipboard_write',
@@ -730,7 +779,7 @@ export function parseCommand(value: unknown): Command {
730
779
  text: string(message['text'], 'clipboard text', MAX_CLIPBOARD_BYTES),
731
780
  }
732
781
  case 'register_global_shortcut':
733
- keys(message, ['id', 'type', 'window_id', 'shortcut_id', 'key', 'modifiers'])
782
+ keys(message, COMMAND_REGISTER_GLOBAL_SHORTCUT_KEYS)
734
783
  return {
735
784
  id: request,
736
785
  type: 'register_global_shortcut',
@@ -740,7 +789,7 @@ export function parseCommand(value: unknown): Command {
740
789
  modifiers: parseShortcutModifiers(message['modifiers']),
741
790
  }
742
791
  case 'unregister_global_shortcut':
743
- keys(message, ['id', 'type', 'window_id', 'shortcut_id'])
792
+ keys(message, COMMAND_UNREGISTER_GLOBAL_SHORTCUT_KEYS)
744
793
  return {
745
794
  id: request,
746
795
  type: 'unregister_global_shortcut',
@@ -748,7 +797,7 @@ export function parseCommand(value: unknown): Command {
748
797
  shortcut_id: id(message['shortcut_id']),
749
798
  }
750
799
  case 'post_notification': {
751
- keys(message, ['id', 'type', 'window_id', 'notification_id', 'title', 'body'])
800
+ keys(message, COMMAND_POST_NOTIFICATION_KEYS)
752
801
  const { title, body } = parseNotificationContent(message['title'], message['body'])
753
802
  return {
754
803
  id: request,
@@ -760,7 +809,7 @@ export function parseCommand(value: unknown): Command {
760
809
  }
761
810
  }
762
811
  case 'withdraw_notification':
763
- keys(message, ['id', 'type', 'window_id', 'notification_id'])
812
+ keys(message, COMMAND_WITHDRAW_NOTIFICATION_KEYS)
764
813
  return {
765
814
  id: request,
766
815
  type: 'withdraw_notification',
@@ -768,7 +817,7 @@ export function parseCommand(value: unknown): Command {
768
817
  notification_id: id(message['notification_id']),
769
818
  }
770
819
  case 'open_file_grant':
771
- keys(message, ['id', 'type', 'window_id', 'grant_id', 'root', 'access'])
820
+ keys(message, COMMAND_OPEN_FILE_GRANT_KEYS)
772
821
  return {
773
822
  id: request,
774
823
  type: 'open_file_grant',
@@ -778,7 +827,7 @@ export function parseCommand(value: unknown): Command {
778
827
  access: parseFileAccess(message['access']),
779
828
  }
780
829
  case 'close_file_grant':
781
- keys(message, ['id', 'type', 'window_id', 'grant_id'])
830
+ keys(message, COMMAND_CLOSE_FILE_GRANT_KEYS)
782
831
  return {
783
832
  id: request,
784
833
  type: 'close_file_grant',
@@ -786,7 +835,7 @@ export function parseCommand(value: unknown): Command {
786
835
  grant_id: id(message['grant_id']),
787
836
  }
788
837
  case 'file_request': {
789
- keys(message, ['id', 'type', 'window_id', 'grant_id', 'path', 'operation'])
838
+ keys(message, COMMAND_FILE_REQUEST_KEYS)
790
839
  const path = parseFilePath(message['path'])
791
840
  return {
792
841
  id: request,
@@ -798,7 +847,7 @@ export function parseCommand(value: unknown): Command {
798
847
  }
799
848
  }
800
849
  case 'spawn_process':
801
- keys(message, ['id', 'type', 'window_id', 'process_id', 'launch'])
850
+ keys(message, COMMAND_SPAWN_PROCESS_KEYS)
802
851
  return {
803
852
  id: request,
804
853
  type: 'spawn_process',
@@ -807,7 +856,7 @@ export function parseCommand(value: unknown): Command {
807
856
  launch: parseProcessLaunch(message['launch']),
808
857
  }
809
858
  case 'write_process':
810
- keys(message, ['id', 'type', 'window_id', 'process_id', 'data'])
859
+ keys(message, COMMAND_WRITE_PROCESS_KEYS)
811
860
  return {
812
861
  id: request,
813
862
  type: 'write_process',
@@ -817,7 +866,7 @@ export function parseCommand(value: unknown): Command {
817
866
  }
818
867
  case 'close_process_input':
819
868
  case 'release_process':
820
- keys(message, ['id', 'type', 'window_id', 'process_id'])
869
+ keys(message, COMMAND_CLOSE_PROCESS_INPUT_KEYS)
821
870
  return {
822
871
  id: request,
823
872
  type: message['type'],
@@ -825,7 +874,7 @@ export function parseCommand(value: unknown): Command {
825
874
  process_id: id(message['process_id']),
826
875
  }
827
876
  case 'resize_process':
828
- keys(message, ['id', 'type', 'window_id', 'process_id', 'size'])
877
+ keys(message, COMMAND_RESIZE_PROCESS_KEYS)
829
878
  return {
830
879
  id: request,
831
880
  type: 'resize_process',
@@ -834,7 +883,7 @@ export function parseCommand(value: unknown): Command {
834
883
  size: parseTerminalSize(message['size']),
835
884
  }
836
885
  case 'signal_process':
837
- keys(message, ['id', 'type', 'window_id', 'process_id', 'signal'])
886
+ keys(message, COMMAND_SIGNAL_PROCESS_KEYS)
838
887
  return {
839
888
  id: request,
840
889
  type: 'signal_process',
@@ -843,7 +892,7 @@ export function parseCommand(value: unknown): Command {
843
892
  signal: parseProcessSignal(message['signal']),
844
893
  }
845
894
  case 'acknowledge_process_output':
846
- keys(message, ['id', 'type', 'window_id', 'process_id', 'chunk'])
895
+ keys(message, COMMAND_ACKNOWLEDGE_PROCESS_OUTPUT_KEYS)
847
896
  return {
848
897
  id: request,
849
898
  type: 'acknowledge_process_output',
@@ -852,7 +901,7 @@ export function parseCommand(value: unknown): Command {
852
901
  chunk: id(message['chunk']),
853
902
  }
854
903
  case 'show_file_dialog':
855
- keys(message, ['id', 'type', 'window_id', 'dialog_id', 'options'])
904
+ keys(message, COMMAND_SHOW_FILE_DIALOG_KEYS)
856
905
  return {
857
906
  id: request,
858
907
  type: 'show_file_dialog',
@@ -861,7 +910,7 @@ export function parseCommand(value: unknown): Command {
861
910
  options: parseFileDialogOptions(message['options']),
862
911
  }
863
912
  case 'close_file_dialog':
864
- keys(message, ['id', 'type', 'window_id', 'dialog_id'])
913
+ keys(message, COMMAND_CLOSE_FILE_DIALOG_KEYS)
865
914
  return {
866
915
  id: request,
867
916
  type: 'close_file_dialog',
@@ -869,7 +918,7 @@ export function parseCommand(value: unknown): Command {
869
918
  dialog_id: id(message['dialog_id']),
870
919
  }
871
920
  case 'show_message_dialog':
872
- keys(message, ['id', 'type', 'window_id', 'dialog_id', 'options'])
921
+ keys(message, COMMAND_SHOW_MESSAGE_DIALOG_KEYS)
873
922
  return {
874
923
  id: request,
875
924
  type: 'show_message_dialog',
@@ -878,7 +927,7 @@ export function parseCommand(value: unknown): Command {
878
927
  options: parseMessageDialogOptions(message['options']),
879
928
  }
880
929
  case 'close_message_dialog':
881
- keys(message, ['id', 'type', 'window_id', 'dialog_id'])
930
+ keys(message, COMMAND_CLOSE_MESSAGE_DIALOG_KEYS)
882
931
  return {
883
932
  id: request,
884
933
  type: 'close_message_dialog',
@@ -886,7 +935,7 @@ export function parseCommand(value: unknown): Command {
886
935
  dialog_id: id(message['dialog_id']),
887
936
  }
888
937
  case 'show_context_menu':
889
- keys(message, ['id', 'type', 'window_id', 'menu_id', 'x', 'y', 'items'])
938
+ keys(message, COMMAND_SHOW_CONTEXT_MENU_KEYS)
890
939
  return {
891
940
  id: request,
892
941
  type: 'show_context_menu',
@@ -897,7 +946,7 @@ export function parseCommand(value: unknown): Command {
897
946
  items: parseContextMenuItems(message['items']),
898
947
  }
899
948
  case 'close_context_menu':
900
- keys(message, ['id', 'type', 'window_id', 'menu_id'])
949
+ keys(message, COMMAND_CLOSE_CONTEXT_MENU_KEYS)
901
950
  return {
902
951
  id: request,
903
952
  type: 'close_context_menu',
@@ -905,7 +954,7 @@ export function parseCommand(value: unknown): Command {
905
954
  menu_id: id(message['menu_id']),
906
955
  }
907
956
  case 'register_drop_target':
908
- keys(message, ['id', 'type', 'window_id', 'target_id', 'node_id'])
957
+ keys(message, COMMAND_REGISTER_DROP_TARGET_KEYS)
909
958
  return {
910
959
  id: request,
911
960
  type: 'register_drop_target',
@@ -914,7 +963,7 @@ export function parseCommand(value: unknown): Command {
914
963
  node_id: id(message['node_id']),
915
964
  }
916
965
  case 'unregister_drop_target':
917
- keys(message, ['id', 'type', 'window_id', 'target_id'])
966
+ keys(message, COMMAND_UNREGISTER_DROP_TARGET_KEYS)
918
967
  return {
919
968
  id: request,
920
969
  type: 'unregister_drop_target',
@@ -922,7 +971,7 @@ export function parseCommand(value: unknown): Command {
922
971
  target_id: id(message['target_id']),
923
972
  }
924
973
  case 'resolve_dropped_item':
925
- keys(message, ['id', 'type', 'window_id', 'target_id', 'drop_id', 'item', 'root'])
974
+ keys(message, COMMAND_RESOLVE_DROPPED_ITEM_KEYS)
926
975
  return {
927
976
  id: request,
928
977
  type: 'resolve_dropped_item',
@@ -933,7 +982,7 @@ export function parseCommand(value: unknown): Command {
933
982
  root: parseRootId(message['root']),
934
983
  }
935
984
  case 'set_app_menu':
936
- keys(message, ['id', 'type', 'menu_id', 'menus'])
985
+ keys(message, COMMAND_SET_APP_MENU_KEYS)
937
986
  return {
938
987
  id: request,
939
988
  type: 'set_app_menu',
@@ -941,16 +990,18 @@ export function parseCommand(value: unknown): Command {
941
990
  menus: parseAppMenus(message['menus']),
942
991
  }
943
992
  case 'shutdown':
944
- keys(message, ['id', 'type'])
993
+ keys(message, COMMAND_SHUTDOWN_KEYS)
945
994
  return { id: request, type: 'shutdown' }
946
995
  default:
947
996
  throw new ProtocolError('Unknown native command')
948
997
  }
949
998
  }
950
999
 
1000
+ const LIMITS_KEYS = fields(...Object.keys(DEFAULT_LIMITS))
1001
+
951
1002
  function limits(value: unknown): Limits {
952
1003
  const data = object(value)
953
- keys(data, Object.keys(DEFAULT_LIMITS))
1004
+ keys(data, LIMITS_KEYS)
954
1005
  const parse = (key: keyof Limits) => integer(data[key], key, 1, DEFAULT_LIMITS[key])
955
1006
  return {
956
1007
  max_frame_bytes: parse('max_frame_bytes'),
@@ -974,13 +1025,21 @@ function limits(value: unknown): Limits {
974
1025
 
975
1026
  const ROUTE_KEYS = ['type', 'sequence', 'window_id', 'node_id', 'tree_revision', 'path'] as const
976
1027
 
1028
+ /** Short paths, the common case, compare pairwise instead of building a set. */
1029
+ function unique(path: readonly number[]): boolean {
1030
+ if (path.length > 16) return new Set(path).size === path.length
1031
+ for (let index = 1; index < path.length; index++)
1032
+ for (let other = 0; other < index; other++) if (path[index] === path[other]) return false
1033
+ return true
1034
+ }
1035
+
977
1036
  function route(message: Record<string, unknown>): NodeEventRoute {
978
1037
  const node_id = id(message['node_id'])
979
1038
  const value = message['path']
980
1039
  if (!Array.isArray(value) || !value.length || value.length > MAX_DEPTH + 1)
981
1040
  throw new ProtocolError('Native event path must contain between 1 and 65 node identities')
982
1041
  const path = value.map(id)
983
- if (new Set(path).size !== path.length || path.at(-1) !== node_id)
1042
+ if (!unique(path) || path.at(-1) !== node_id)
984
1043
  throw new ProtocolError('Native event path must be unique and end at its target')
985
1044
  return {
986
1045
  sequence: id(message['sequence']),
@@ -991,9 +1050,11 @@ function route(message: Record<string, unknown>): NodeEventRoute {
991
1050
  }
992
1051
  }
993
1052
 
1053
+ const MODIFIERS_KEYS = fields('control', 'alt', 'shift', 'platform', 'function')
1054
+
994
1055
  function modifiers(value: unknown): KeyModifiers {
995
1056
  const data = object(value)
996
- keys(data, ['control', 'alt', 'shift', 'platform', 'function'])
1057
+ keys(data, MODIFIERS_KEYS)
997
1058
  const modifier = (name: keyof KeyModifiers) => {
998
1059
  if (typeof data[name] !== 'boolean') throw new ProtocolError('Invalid native key modifier')
999
1060
  return data[name]
@@ -1007,12 +1068,53 @@ function modifiers(value: unknown): KeyModifiers {
1007
1068
  }
1008
1069
  }
1009
1070
 
1071
+ const EVENT_READY_KEYS = fields('type', 'protocol', 'capabilities', 'limits', 'service_policy')
1072
+ const EVENT_ACK_KEYS = fields('type', 'id')
1073
+ const EVENT_OPENED_KEYS = windowReply('root_id', 'tree_revision')
1074
+ const EVENT_APPLIED_KEYS = windowReply('tree_revision')
1075
+ const EVENT_DISPLAY_SNAPSHOT_KEYS = windowReply('snapshot')
1076
+ const EVENT_CLIPBOARD_TEXT_KEYS = windowReply('result')
1077
+ const EVENT_CLIPBOARD_WRITTEN_KEYS = windowReply()
1078
+ const EVENT_GLOBAL_SHORTCUT_REGISTERED_KEYS = windowReply('shortcut_id')
1079
+ const EVENT_DISPLAY_CHANGED_KEYS = windowNotice('snapshot')
1080
+ const EVENT_GLOBAL_SHORTCUT_KEYS = windowNotice('shortcut_id')
1081
+ const EVENT_NOTIFICATION_POSTED_KEYS = windowReply('notification_id', 'activatable')
1082
+ const EVENT_NOTIFICATION_WITHDRAWN_KEYS = windowReply('notification_id')
1083
+ const EVENT_NOTIFICATION_ACTIVATED_KEYS = windowNotice('notification_id')
1084
+ const EVENT_NOTIFICATION_DISMISSED_KEYS = windowNotice('notification_id', 'reason')
1085
+ const EVENT_FILE_GRANT_OPENED_KEYS = windowReply('grant_id')
1086
+ const EVENT_FILE_RESULT_KEYS = windowReply('grant_id', 'result')
1087
+ const EVENT_PROCESS_SPAWNED_KEYS = windowReply('process_id')
1088
+ const EVENT_PROCESS_OUTPUT_KEYS = windowNotice('process_id', 'chunk', 'stream', 'data')
1089
+ const EVENT_PROCESS_EXITED_KEYS = windowNotice('process_id', 'exit')
1090
+ const EVENT_FILE_DIALOG_OPENED_KEYS = windowReply('dialog_id')
1091
+ const EVENT_FILE_DIALOG_COMPLETED_KEYS = windowNotice('dialog_id', 'result')
1092
+ const EVENT_MESSAGE_DIALOG_OPENED_KEYS = windowReply('dialog_id')
1093
+ const EVENT_MESSAGE_DIALOG_COMPLETED_KEYS = windowNotice('dialog_id', 'result')
1094
+ const EVENT_CONTEXT_MENU_OPENED_KEYS = windowReply('menu_id')
1095
+ const EVENT_CONTEXT_MENU_COMPLETED_KEYS = windowNotice('menu_id', 'item_id')
1096
+ const EVENT_DROP_TARGET_REGISTERED_KEYS = windowReply('target_id')
1097
+ const EVENT_DROPPED_ITEM_RESOLVED_KEYS = windowReply('target_id', 'drop_id', 'item', 'root', 'path')
1098
+ const EVENT_DROP_TARGET_DRAG_KEYS = windowNotice('target_id', 'state')
1099
+ const EVENT_DROP_RECEIVED_KEYS = windowNotice('target_id', 'drop_id', 'items')
1100
+ const EVENT_DROP_REJECTED_KEYS = windowNotice('target_id', 'reason')
1101
+ const EVENT_ROUTE_KEYS = fields(...ROUTE_KEYS)
1102
+ const EVENT_KEY_KEYS = fields(...ROUTE_KEYS, 'key', 'key_char', 'modifiers')
1103
+ const EVENT_POINTER_KEYS = fields(...ROUTE_KEYS, 'button', 'x', 'y', 'modifiers')
1104
+ const EVENT_DISMISS_KEYS = fields(...ROUTE_KEYS, 'reason')
1105
+ const EVENT_SPLIT_RESIZE_KEYS = fields(...ROUTE_KEYS, 'ratio', 'source')
1106
+ const EVENT_CLOSED_KEYS = windowNotice()
1107
+ const EVENT_INPUT_KEYS = fields(...ROUTE_KEYS, 'value', 'input_revision')
1108
+ const EVENT_APP_MENU_SET_KEYS = fields('type', 'id', 'menu_id')
1109
+ const EVENT_APP_MENU_ITEM_KEYS = fields('type', 'sequence', 'menu_id', 'item_id')
1110
+ const EVENT_ERROR_KEYS = fields('type', 'id', 'code', 'message')
1111
+
1010
1112
  /** Native stdout is untrusted until its message shape and limits are checked. */
1011
1113
  export function parseEvent(value: unknown): NativeEvent {
1012
1114
  const message = object(value)
1013
1115
  switch (message['type']) {
1014
1116
  case 'ready': {
1015
- keys(message, ['type', 'protocol', 'capabilities', 'limits', 'service_policy'])
1117
+ keys(message, EVENT_READY_KEYS)
1016
1118
  const capabilities = message['capabilities']
1017
1119
  if (!Array.isArray(capabilities) || capabilities.length > CAPABILITIES.length)
1018
1120
  throw new ProtocolError('Invalid native capabilities')
@@ -1032,10 +1134,10 @@ export function parseEvent(value: unknown): NativeEvent {
1032
1134
  }
1033
1135
  }
1034
1136
  case 'ack':
1035
- keys(message, ['type', 'id'])
1137
+ keys(message, EVENT_ACK_KEYS)
1036
1138
  return { type: 'ack', id: id(message['id']) }
1037
1139
  case 'opened':
1038
- keys(message, ['type', 'id', 'window_id', 'root_id', 'tree_revision'])
1140
+ keys(message, EVENT_OPENED_KEYS)
1039
1141
  return {
1040
1142
  type: 'opened',
1041
1143
  id: id(message['id']),
@@ -1044,7 +1146,7 @@ export function parseEvent(value: unknown): NativeEvent {
1044
1146
  tree_revision: integer(message['tree_revision'], 'tree revision'),
1045
1147
  }
1046
1148
  case 'applied':
1047
- keys(message, ['type', 'id', 'window_id', 'tree_revision'])
1149
+ keys(message, EVENT_APPLIED_KEYS)
1048
1150
  return {
1049
1151
  type: 'applied',
1050
1152
  id: id(message['id']),
@@ -1052,7 +1154,7 @@ export function parseEvent(value: unknown): NativeEvent {
1052
1154
  tree_revision: integer(message['tree_revision'], 'tree revision'),
1053
1155
  }
1054
1156
  case 'display_snapshot':
1055
- keys(message, ['type', 'id', 'window_id', 'snapshot'])
1157
+ keys(message, EVENT_DISPLAY_SNAPSHOT_KEYS)
1056
1158
  return {
1057
1159
  type: 'display_snapshot',
1058
1160
  id: id(message['id']),
@@ -1060,7 +1162,7 @@ export function parseEvent(value: unknown): NativeEvent {
1060
1162
  snapshot: parseDisplaySnapshot(message['snapshot']),
1061
1163
  }
1062
1164
  case 'clipboard_text':
1063
- keys(message, ['type', 'id', 'window_id', 'result'])
1165
+ keys(message, EVENT_CLIPBOARD_TEXT_KEYS)
1064
1166
  return {
1065
1167
  type: 'clipboard_text',
1066
1168
  id: id(message['id']),
@@ -1068,7 +1170,7 @@ export function parseEvent(value: unknown): NativeEvent {
1068
1170
  result: parseClipboardText(message['result']),
1069
1171
  }
1070
1172
  case 'clipboard_written':
1071
- keys(message, ['type', 'id', 'window_id'])
1173
+ keys(message, EVENT_CLIPBOARD_WRITTEN_KEYS)
1072
1174
  return {
1073
1175
  type: 'clipboard_written',
1074
1176
  id: id(message['id']),
@@ -1076,7 +1178,7 @@ export function parseEvent(value: unknown): NativeEvent {
1076
1178
  }
1077
1179
  case 'global_shortcut_registered':
1078
1180
  case 'global_shortcut_unregistered':
1079
- keys(message, ['type', 'id', 'window_id', 'shortcut_id'])
1181
+ keys(message, EVENT_GLOBAL_SHORTCUT_REGISTERED_KEYS)
1080
1182
  return {
1081
1183
  type: message['type'],
1082
1184
  id: id(message['id']),
@@ -1084,7 +1186,7 @@ export function parseEvent(value: unknown): NativeEvent {
1084
1186
  shortcut_id: id(message['shortcut_id']),
1085
1187
  }
1086
1188
  case 'display_changed':
1087
- keys(message, ['type', 'sequence', 'window_id', 'snapshot'])
1189
+ keys(message, EVENT_DISPLAY_CHANGED_KEYS)
1088
1190
  return {
1089
1191
  type: 'display_changed',
1090
1192
  sequence: id(message['sequence']),
@@ -1092,7 +1194,7 @@ export function parseEvent(value: unknown): NativeEvent {
1092
1194
  snapshot: parseDisplaySnapshot(message['snapshot']),
1093
1195
  }
1094
1196
  case 'global_shortcut':
1095
- keys(message, ['type', 'sequence', 'window_id', 'shortcut_id'])
1197
+ keys(message, EVENT_GLOBAL_SHORTCUT_KEYS)
1096
1198
  return {
1097
1199
  type: 'global_shortcut',
1098
1200
  sequence: id(message['sequence']),
@@ -1100,7 +1202,7 @@ export function parseEvent(value: unknown): NativeEvent {
1100
1202
  shortcut_id: id(message['shortcut_id']),
1101
1203
  }
1102
1204
  case 'notification_posted':
1103
- keys(message, ['type', 'id', 'window_id', 'notification_id', 'activatable'])
1205
+ keys(message, EVENT_NOTIFICATION_POSTED_KEYS)
1104
1206
  if (typeof message['activatable'] !== 'boolean')
1105
1207
  throw new ProtocolError('Notification activatable must be a boolean')
1106
1208
  return {
@@ -1111,7 +1213,7 @@ export function parseEvent(value: unknown): NativeEvent {
1111
1213
  activatable: message['activatable'],
1112
1214
  }
1113
1215
  case 'notification_withdrawn':
1114
- keys(message, ['type', 'id', 'window_id', 'notification_id'])
1216
+ keys(message, EVENT_NOTIFICATION_WITHDRAWN_KEYS)
1115
1217
  return {
1116
1218
  type: 'notification_withdrawn',
1117
1219
  id: id(message['id']),
@@ -1119,7 +1221,7 @@ export function parseEvent(value: unknown): NativeEvent {
1119
1221
  notification_id: id(message['notification_id']),
1120
1222
  }
1121
1223
  case 'notification_activated':
1122
- keys(message, ['type', 'sequence', 'window_id', 'notification_id'])
1224
+ keys(message, EVENT_NOTIFICATION_ACTIVATED_KEYS)
1123
1225
  return {
1124
1226
  type: 'notification_activated',
1125
1227
  sequence: id(message['sequence']),
@@ -1127,7 +1229,7 @@ export function parseEvent(value: unknown): NativeEvent {
1127
1229
  notification_id: id(message['notification_id']),
1128
1230
  }
1129
1231
  case 'notification_dismissed':
1130
- keys(message, ['type', 'sequence', 'window_id', 'notification_id', 'reason'])
1232
+ keys(message, EVENT_NOTIFICATION_DISMISSED_KEYS)
1131
1233
  return {
1132
1234
  type: 'notification_dismissed',
1133
1235
  sequence: id(message['sequence']),
@@ -1137,7 +1239,7 @@ export function parseEvent(value: unknown): NativeEvent {
1137
1239
  }
1138
1240
  case 'file_grant_opened':
1139
1241
  case 'file_grant_closed':
1140
- keys(message, ['type', 'id', 'window_id', 'grant_id'])
1242
+ keys(message, EVENT_FILE_GRANT_OPENED_KEYS)
1141
1243
  return {
1142
1244
  type: message['type'],
1143
1245
  id: id(message['id']),
@@ -1145,7 +1247,7 @@ export function parseEvent(value: unknown): NativeEvent {
1145
1247
  grant_id: id(message['grant_id']),
1146
1248
  }
1147
1249
  case 'file_result':
1148
- keys(message, ['type', 'id', 'window_id', 'grant_id', 'result'])
1250
+ keys(message, EVENT_FILE_RESULT_KEYS)
1149
1251
  return {
1150
1252
  type: 'file_result',
1151
1253
  id: id(message['id']),
@@ -1156,7 +1258,7 @@ export function parseEvent(value: unknown): NativeEvent {
1156
1258
  case 'process_spawned':
1157
1259
  case 'process_updated':
1158
1260
  case 'process_released':
1159
- keys(message, ['type', 'id', 'window_id', 'process_id'])
1261
+ keys(message, EVENT_PROCESS_SPAWNED_KEYS)
1160
1262
  return {
1161
1263
  type: message['type'],
1162
1264
  id: id(message['id']),
@@ -1164,7 +1266,7 @@ export function parseEvent(value: unknown): NativeEvent {
1164
1266
  process_id: id(message['process_id']),
1165
1267
  }
1166
1268
  case 'process_output':
1167
- keys(message, ['type', 'sequence', 'window_id', 'process_id', 'chunk', 'stream', 'data'])
1269
+ keys(message, EVENT_PROCESS_OUTPUT_KEYS)
1168
1270
  return {
1169
1271
  type: 'process_output',
1170
1272
  sequence: id(message['sequence']),
@@ -1175,7 +1277,7 @@ export function parseEvent(value: unknown): NativeEvent {
1175
1277
  data: parseProcessOutput(message['data']),
1176
1278
  }
1177
1279
  case 'process_exited':
1178
- keys(message, ['type', 'sequence', 'window_id', 'process_id', 'exit'])
1280
+ keys(message, EVENT_PROCESS_EXITED_KEYS)
1179
1281
  return {
1180
1282
  type: 'process_exited',
1181
1283
  sequence: id(message['sequence']),
@@ -1185,7 +1287,7 @@ export function parseEvent(value: unknown): NativeEvent {
1185
1287
  }
1186
1288
  case 'file_dialog_opened':
1187
1289
  case 'file_dialog_closed':
1188
- keys(message, ['type', 'id', 'window_id', 'dialog_id'])
1290
+ keys(message, EVENT_FILE_DIALOG_OPENED_KEYS)
1189
1291
  return {
1190
1292
  type: message['type'],
1191
1293
  id: id(message['id']),
@@ -1193,7 +1295,7 @@ export function parseEvent(value: unknown): NativeEvent {
1193
1295
  dialog_id: id(message['dialog_id']),
1194
1296
  }
1195
1297
  case 'file_dialog_completed':
1196
- keys(message, ['type', 'sequence', 'window_id', 'dialog_id', 'result'])
1298
+ keys(message, EVENT_FILE_DIALOG_COMPLETED_KEYS)
1197
1299
  return {
1198
1300
  type: 'file_dialog_completed',
1199
1301
  sequence: id(message['sequence']),
@@ -1203,7 +1305,7 @@ export function parseEvent(value: unknown): NativeEvent {
1203
1305
  }
1204
1306
  case 'message_dialog_opened':
1205
1307
  case 'message_dialog_closed':
1206
- keys(message, ['type', 'id', 'window_id', 'dialog_id'])
1308
+ keys(message, EVENT_MESSAGE_DIALOG_OPENED_KEYS)
1207
1309
  return {
1208
1310
  type: message['type'],
1209
1311
  id: id(message['id']),
@@ -1211,7 +1313,7 @@ export function parseEvent(value: unknown): NativeEvent {
1211
1313
  dialog_id: id(message['dialog_id']),
1212
1314
  }
1213
1315
  case 'message_dialog_completed':
1214
- keys(message, ['type', 'sequence', 'window_id', 'dialog_id', 'result'])
1316
+ keys(message, EVENT_MESSAGE_DIALOG_COMPLETED_KEYS)
1215
1317
  return {
1216
1318
  type: 'message_dialog_completed',
1217
1319
  sequence: id(message['sequence']),
@@ -1221,7 +1323,7 @@ export function parseEvent(value: unknown): NativeEvent {
1221
1323
  }
1222
1324
  case 'context_menu_opened':
1223
1325
  case 'context_menu_closed':
1224
- keys(message, ['type', 'id', 'window_id', 'menu_id'])
1326
+ keys(message, EVENT_CONTEXT_MENU_OPENED_KEYS)
1225
1327
  return {
1226
1328
  type: message['type'],
1227
1329
  id: id(message['id']),
@@ -1229,7 +1331,7 @@ export function parseEvent(value: unknown): NativeEvent {
1229
1331
  menu_id: id(message['menu_id']),
1230
1332
  }
1231
1333
  case 'context_menu_completed': {
1232
- keys(message, ['type', 'sequence', 'window_id', 'menu_id', 'item_id'])
1334
+ keys(message, EVENT_CONTEXT_MENU_COMPLETED_KEYS)
1233
1335
  const event: Extract<NativeEvent, { type: 'context_menu_completed' }> = {
1234
1336
  type: 'context_menu_completed',
1235
1337
  sequence: id(message['sequence']),
@@ -1241,7 +1343,7 @@ export function parseEvent(value: unknown): NativeEvent {
1241
1343
  }
1242
1344
  case 'drop_target_registered':
1243
1345
  case 'drop_target_unregistered':
1244
- keys(message, ['type', 'id', 'window_id', 'target_id'])
1346
+ keys(message, EVENT_DROP_TARGET_REGISTERED_KEYS)
1245
1347
  return {
1246
1348
  type: message['type'],
1247
1349
  id: id(message['id']),
@@ -1249,7 +1351,7 @@ export function parseEvent(value: unknown): NativeEvent {
1249
1351
  target_id: id(message['target_id']),
1250
1352
  }
1251
1353
  case 'dropped_item_resolved':
1252
- keys(message, ['type', 'id', 'window_id', 'target_id', 'drop_id', 'item', 'root', 'path'])
1354
+ keys(message, EVENT_DROPPED_ITEM_RESOLVED_KEYS)
1253
1355
  return {
1254
1356
  type: 'dropped_item_resolved',
1255
1357
  id: id(message['id']),
@@ -1261,7 +1363,7 @@ export function parseEvent(value: unknown): NativeEvent {
1261
1363
  path: parseFilePath(message['path']),
1262
1364
  }
1263
1365
  case 'drop_target_drag':
1264
- keys(message, ['type', 'sequence', 'window_id', 'target_id', 'state'])
1366
+ keys(message, EVENT_DROP_TARGET_DRAG_KEYS)
1265
1367
  return {
1266
1368
  type: 'drop_target_drag',
1267
1369
  sequence: id(message['sequence']),
@@ -1270,7 +1372,7 @@ export function parseEvent(value: unknown): NativeEvent {
1270
1372
  state: parseDragState(message['state']),
1271
1373
  }
1272
1374
  case 'drop_received':
1273
- keys(message, ['type', 'sequence', 'window_id', 'target_id', 'drop_id', 'items'])
1375
+ keys(message, EVENT_DROP_RECEIVED_KEYS)
1274
1376
  return {
1275
1377
  type: 'drop_received',
1276
1378
  sequence: id(message['sequence']),
@@ -1280,7 +1382,7 @@ export function parseEvent(value: unknown): NativeEvent {
1280
1382
  items: parseDroppedItems(message['items']),
1281
1383
  }
1282
1384
  case 'drop_rejected':
1283
- keys(message, ['type', 'sequence', 'window_id', 'target_id', 'reason'])
1385
+ keys(message, EVENT_DROP_REJECTED_KEYS)
1284
1386
  return {
1285
1387
  type: 'drop_rejected',
1286
1388
  sequence: id(message['sequence']),
@@ -1291,14 +1393,14 @@ export function parseEvent(value: unknown): NativeEvent {
1291
1393
  case 'click':
1292
1394
  case 'focus':
1293
1395
  case 'blur':
1294
- keys(message, ROUTE_KEYS)
1396
+ keys(message, EVENT_ROUTE_KEYS)
1295
1397
  return {
1296
1398
  type: message['type'],
1297
1399
  ...route(message),
1298
1400
  }
1299
1401
  case 'key_down':
1300
1402
  case 'key_up': {
1301
- keys(message, [...ROUTE_KEYS, 'key', 'key_char', 'modifiers'])
1403
+ keys(message, EVENT_KEY_KEYS)
1302
1404
  const key = string(message['key'], 'key', 256)
1303
1405
  if (!key) throw new ProtocolError('Native key must be nonempty')
1304
1406
  return {
@@ -1313,7 +1415,7 @@ export function parseEvent(value: unknown): NativeEvent {
1313
1415
  }
1314
1416
  case 'pointer_down':
1315
1417
  case 'pointer_up': {
1316
- keys(message, [...ROUTE_KEYS, 'button', 'x', 'y', 'modifiers'])
1418
+ keys(message, EVENT_POINTER_KEYS)
1317
1419
  const button = (['left', 'right', 'middle', 'back', 'forward'] as const).find(
1318
1420
  (button) => button === message['button']
1319
1421
  )
@@ -1328,12 +1430,12 @@ export function parseEvent(value: unknown): NativeEvent {
1328
1430
  }
1329
1431
  }
1330
1432
  case 'dismiss':
1331
- keys(message, [...ROUTE_KEYS, 'reason'])
1433
+ keys(message, EVENT_DISMISS_KEYS)
1332
1434
  if (message['reason'] !== 'window_blur')
1333
1435
  throw new ProtocolError('Unknown native dismissal reason')
1334
1436
  return { type: 'dismiss', ...route(message), reason: 'window_blur' }
1335
1437
  case 'split_resize': {
1336
- keys(message, [...ROUTE_KEYS, 'ratio', 'source'])
1438
+ keys(message, EVENT_SPLIT_RESIZE_KEYS)
1337
1439
  const source = (['pointer', 'keyboard'] as const).find(
1338
1440
  (source) => source === message['source']
1339
1441
  )
@@ -1346,14 +1448,14 @@ export function parseEvent(value: unknown): NativeEvent {
1346
1448
  }
1347
1449
  }
1348
1450
  case 'closed':
1349
- keys(message, ['type', 'sequence', 'window_id'])
1451
+ keys(message, EVENT_CLOSED_KEYS)
1350
1452
  return {
1351
1453
  type: 'closed',
1352
1454
  sequence: id(message['sequence']),
1353
1455
  window_id: id(message['window_id']),
1354
1456
  }
1355
1457
  case 'input':
1356
- keys(message, [...ROUTE_KEYS, 'value', 'input_revision'])
1458
+ keys(message, EVENT_INPUT_KEYS)
1357
1459
  return {
1358
1460
  type: 'input',
1359
1461
  ...route(message),
@@ -1361,10 +1463,10 @@ export function parseEvent(value: unknown): NativeEvent {
1361
1463
  input_revision: integer(message['input_revision'], 'input revision'),
1362
1464
  }
1363
1465
  case 'app_menu_set':
1364
- keys(message, ['type', 'id', 'menu_id'])
1466
+ keys(message, EVENT_APP_MENU_SET_KEYS)
1365
1467
  return { type: 'app_menu_set', id: id(message['id']), menu_id: id(message['menu_id']) }
1366
1468
  case 'app_menu_item':
1367
- keys(message, ['type', 'sequence', 'menu_id', 'item_id'])
1469
+ keys(message, EVENT_APP_MENU_ITEM_KEYS)
1368
1470
  return {
1369
1471
  type: 'app_menu_item',
1370
1472
  sequence: id(message['sequence']),
@@ -1372,7 +1474,7 @@ export function parseEvent(value: unknown): NativeEvent {
1372
1474
  item_id: parseAppMenuItemId(message['item_id']),
1373
1475
  }
1374
1476
  case 'error': {
1375
- keys(message, ['type', 'id', 'code', 'message'])
1477
+ keys(message, EVENT_ERROR_KEYS)
1376
1478
  const code = ERROR_CODES.find((code) => code === message['code'])
1377
1479
  if (!code) throw new ProtocolError('Invalid native error code')
1378
1480
  return {