ntk 8.2.0 → 8.3.0

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/lib/app.js CHANGED
@@ -4,11 +4,15 @@ import { GLError, backendFor, glCapabilities, glError, resolveGLPolicy } from '.
4
4
  import { chooseGLXConfig } from './glx.js';
5
5
  import Picture from './picture.js';
6
6
  import Pixmap from './pixmap.js';
7
+ import Region, { REGION_DOCS } from './region.js';
8
+ import { formatForDepth, parsePictFormats, visualDepths, visualFormats } from './pictformat.js';
7
9
  import { DEFAULT_RASTER_POLICY, defaultRasterizer } from './rasterize.js';
8
10
  import { dropShadowSurfaces } from './shadow.js';
11
+ import { sharedGlyphsFor } from './sharedglyphs.js';
9
12
  import { ShmUploader } from './shm-upload.js';
10
13
  import FontManager from './text/fontmanager.js';
11
14
  import Window from './window.js';
15
+ import * as xevents from './events_map.js';
12
16
 
13
17
  /**
14
18
  * The highest predefined atom id in the core protocol, `XA_WM_TRANSIENT_FOR`.
@@ -54,6 +58,38 @@ function modeRate(mode) {
54
58
  return mode.dot_clock / (mode.h_total * vTotal);
55
59
  }
56
60
 
61
+ /**
62
+ * XFIXES is missing, so no server-side regions.
63
+ *
64
+ * Worth a sentence about who is likely reading it: every X.Org release since
65
+ * 2004 and every XQuartz ship XFIXES, so a server without it is a deliberately
66
+ * minimal one — an embedded/nested server, or node-x11's own pure-JS server,
67
+ * which implements RENDER but not this. That is a fact about the display, not
68
+ * about the call, which is why it carries a `code`: a host that can degrade
69
+ * should branch on it rather than crash.
70
+ *
71
+ * @param {Error} [cause] whatever the extension query failed with
72
+ */
73
+ function noXFixesError(cause) {
74
+ const err = new Error(
75
+ 'ntk: the X server has no XFIXES extension, so it has no regions.\n' +
76
+ '\n' +
77
+ 'Region clips (ctx.clipRegion), damage regions and window shapes all need it.\n' +
78
+ 'Rectangular and path clips do not — ctx.clip() is core RENDER and works\n' +
79
+ 'everywhere:\n' +
80
+ '\n' +
81
+ ' ctx.save();\n' +
82
+ ' ctx.beginPath();\n' +
83
+ ' ctx.rect(x, y, w, h);\n' +
84
+ ' ctx.clip();\n' +
85
+ '\n' +
86
+ `${REGION_DOCS}`,
87
+ cause ? { cause } : undefined
88
+ );
89
+ err.code = 'ERR_NTK_NO_XFIXES';
90
+ return err;
91
+ }
92
+
57
93
  /**
58
94
  * A connection to an X server. Owns the underlying node-x11 client
59
95
  * (`app.X`) and acts as a factory for windows and pixmaps.
@@ -90,12 +126,34 @@ export default class App {
90
126
  this._isolateAtoms();
91
127
  this._fonts = null;
92
128
  this._clipboard = null;
129
+ // Decide the shared-glyph question now, while the environment the app
130
+ // was created under is what the caller sees: the kill switch and the
131
+ // option are sampled once per connection, not per page. Constructing the
132
+ // client costs no I/O — discovery waits for the first page to bind.
133
+ sharedGlyphsFor(this);
93
134
  this._cursors = null;
94
135
  this._solidPictures = new Map();
95
136
  this._rasterizer = undefined;
96
137
  this._shm = undefined;
97
- this._xinputPromise = null;
138
+ this._extensionPromises = new Map();
139
+ // extension event routing (see _routeExtensionEvents): server-assigned
140
+ // event type code -> events_map.extension entry. Null until the first
141
+ // extension with events is required, which is also when the client-level
142
+ // listener that consults it is attached.
143
+ this._extEvents = null;
98
144
  this._devicesPromise = null;
145
+ // XFIXES, once something has asked for it (regions, `app.fixes()`). The
146
+ // promise lives in _extensionPromises with the other extensions; the
147
+ // resolved object is also kept here because a region clip has to be
148
+ // installed synchronously, in request order with the drawing around it —
149
+ // see RenderingContext2d#clipRegion.
150
+ this._fixes = null;
151
+ // The visual -> picture format table and the formats list it was built
152
+ // from, read once out of node-x11's cached QueryPictFormats reply. Kept
153
+ // resolved alongside the promise so that binding a picture, which happens
154
+ // synchronously, can consult it without waiting (see pictFormats).
155
+ this._pictFormats = null;
156
+ this._pictFormatsPromise = null;
99
157
  // node-x11 emits X errors it cannot route to a request callback as
100
158
  // 'error' on the client — from inside its packet parser. With no
101
159
  // listener that emit throws and the parser never re-arms, silently
@@ -302,6 +360,89 @@ export default class App {
302
360
  });
303
361
  }
304
362
 
363
+ /**
364
+ * The node-x11 object for extension `name`, or `null` where the server has
365
+ * none — asked once per connection, the absent answer included.
366
+ *
367
+ * node-x11 caches the `QueryExtension` reply itself since 4.0.1
368
+ * ([node-x11#287](https://github.com/sidorares/node-x11/issues/287)), so a
369
+ * repeated probe no longer costs a round trip even when the answer is
370
+ * absent — "can this machine run a compositor?" is a question about what is
371
+ * *not* there, and it used to pay for asking. This cache stays for what it
372
+ * does beyond that: one promise per name, so concurrent callers share the
373
+ * one query, `_routeExtensionEvents` runs once rather than per call, and an
374
+ * absent extension answers `null` instead of an error.
375
+ *
376
+ * `name` is node-x11's module name (`'fixes'`, not `'XFIXES'`), and the
377
+ * accessors below are the whole supported set. Deliberately not public: a
378
+ * name that arrives misspelled resolves `null` forever, which is
379
+ * indistinguishable from a server that does not have the extension.
380
+ *
381
+ * @param {string} name
382
+ * @returns {Promise<object|null>}
383
+ */
384
+ _extension(name) {
385
+ let pending = this._extensionPromises.get(name);
386
+ if (!pending) {
387
+ pending = new Promise((resolve) => {
388
+ this.X.require(name, (err, ext) => {
389
+ if (!err && ext) this._routeExtensionEvents(name, ext);
390
+ resolve(err ? null : ext);
391
+ });
392
+ });
393
+ this._extensionPromises.set(name, pending);
394
+ }
395
+ return pending;
396
+ }
397
+
398
+ /**
399
+ * Give this extension's events names, and a route to the object they name.
400
+ *
401
+ * A non-generic extension event carries a server-assigned type code
402
+ * (`firstEvent` + a fixed offset) and names its target under the field its
403
+ * own protocol calls it — DamageNotify a `drawable`, ShapeNotify and the
404
+ * XFIXES notifies a `window` — never under the `wid` node-x11 dispatches
405
+ * per-window consumers by. So without this they reach the client's raw
406
+ * `'event'` stream and nothing else (issue #290).
407
+ *
408
+ * Registered here, when the extension is first required through the
409
+ * accessors above, because the type codes exist only once the server has
410
+ * answered. From then on the listener below hands each one to the Window
411
+ * or Pixmap it names — through `_deliverEvent`, so a window's coalescing
412
+ * and frame pacing apply: `damage` unions its rectangles per paced frame
413
+ * exactly as `expose` does. The lookup goes through `X.event_consumers`,
414
+ * where every Window already sits for core dispatch and a Pixmap enrols
415
+ * itself when given a listener (see lib/pixmap.js).
416
+ */
417
+ _routeExtensionEvents(module, ext) {
418
+ const events = xevents.extension[module];
419
+ if (!events || !ext.firstEvent || !ext.events) return;
420
+ if (!this._extEvents) {
421
+ this._extEvents = new Map();
422
+ this.X.on('event', (ev) => {
423
+ // by the server-assigned type code, which every extension event
424
+ // carries since x11 4.0.0 (node-x11#284 — DamageNotify and a couple
425
+ // of others used to name themselves without it). Only the codes this
426
+ // table registered can match.
427
+ const route = this._extEvents.get(ev.type);
428
+ if (!route) return;
429
+ const target = this.X.event_consumers[ev[route.target]];
430
+ // a consumer that is not an ntk drawable (a caller's own) keeps
431
+ // reading the raw client stream it always read
432
+ if (typeof target?._deliverEvent !== 'function') return;
433
+ const ntkev = route.translate(ev);
434
+ ntkev.target = target;
435
+ if (target instanceof Window) ntkev.window = target;
436
+ target._deliverEvent(route.name, ntkev);
437
+ });
438
+ }
439
+ for (const [key, spec] of Object.entries(events)) {
440
+ const offset = ext.events[key];
441
+ if (offset === undefined) continue;
442
+ this._extEvents.set(ext.firstEvent + offset, spec);
443
+ }
444
+ }
445
+
305
446
  /**
306
447
  * The XInput extension, or `null` where the server has none — asked once
307
448
  * per connection.
@@ -312,12 +453,59 @@ export default class App {
312
453
  * @returns {Promise<object|null>}
313
454
  */
314
455
  xinput() {
315
- if (!this._xinputPromise) {
316
- this._xinputPromise = new Promise((resolve) => {
317
- this.X.require('xinput', (err, ext) => resolve(err ? null : ext));
318
- });
319
- }
320
- return this._xinputPromise;
456
+ return this._extension('xinput');
457
+ }
458
+
459
+ /**
460
+ * The Composite extension, or `null` where the server has none: the
461
+ * redirection half of a compositing manager — `RedirectSubwindows`,
462
+ * `NameWindowPixmap`, `Get`/`ReleaseOverlayWindow`
463
+ * (docs/app.md#extensions).
464
+ *
465
+ * `null` here is a live answer rather than a defensive one, and the one
466
+ * worth branching on: XQuartz carries DAMAGE, XFIXES, SHAPE and RENDER but
467
+ * no Composite at all, so this is where "can this machine run a
468
+ * compositor?" is decided.
469
+ *
470
+ * @returns {Promise<object|null>}
471
+ */
472
+ composite() {
473
+ return this._extension('composite');
474
+ }
475
+
476
+ /**
477
+ * The DAMAGE extension, or `null` where the server has none: what turns
478
+ * "this drawable changed" into an event, so a compositor repaints the
479
+ * region a client drew into rather than the screen
480
+ * (docs/app.md#extensions).
481
+ *
482
+ * @returns {Promise<object|null>}
483
+ */
484
+ damage() {
485
+ return this._extension('damage');
486
+ }
487
+
488
+ /**
489
+ * The XFIXES extension, or `null` where the server has none: server-side
490
+ * regions and the algebra over them, which is how a damaged area becomes a
491
+ * clip — `SetPictureClipRegion(ctx.picture.id, 0, 0, region)` narrows a 2d
492
+ * context to one (docs/app.md#extensions).
493
+ *
494
+ * @returns {Promise<object|null>}
495
+ */
496
+ xfixes() {
497
+ return this._extension('fixes');
498
+ }
499
+
500
+ /**
501
+ * The SHAPE extension, or `null` where the server has none: non-rectangular
502
+ * window bounding/clip/input shapes, which a compositor has to read to
503
+ * paint a shaped client correctly (docs/app.md#extensions).
504
+ *
505
+ * @returns {Promise<object|null>}
506
+ */
507
+ shape() {
508
+ return this._extension('shape');
321
509
  }
322
510
 
323
511
  /**
@@ -355,6 +543,18 @@ export default class App {
355
543
  return this._clipboard;
356
544
  }
357
545
 
546
+ /**
547
+ * The cross-process shared glyph cache client (docs/shared-glyphs.md), or
548
+ * `null` when the feature is off — `createClient({ sharedGlyphs: false })`
549
+ * or the `NTK_NO_SHARED_GLYPHS` environment kill switch. Purely lazy: the
550
+ * accessor itself touches no server state; discovery (and the first app's
551
+ * self-election as the display's glyph directory) happens when the first
552
+ * glyph page binds.
553
+ */
554
+ get sharedGlyphs() {
555
+ return sharedGlyphsFor(this);
556
+ }
557
+
358
558
  /** per-connection cache of X11 cursor-font cursors (see lib/cursor.js) */
359
559
  get cursors() {
360
560
  if (!this._cursors) this._cursors = new CursorCache(this);
@@ -456,6 +656,102 @@ export default class App {
456
656
  return null;
457
657
  }
458
658
 
659
+ /**
660
+ * The RENDER picture formats this server publishes, and which one belongs
661
+ * to each visual: `{ formats, byVisual }`.
662
+ *
663
+ * No round trip of ntk's own: node-x11 sends `QueryPictFormats` itself
664
+ * while requiring RENDER — that is where its `rgb24`/`rgba32`/`a8` come
665
+ * from — and since 4.0.0 it keeps the reply as `Render.pictFormats`. So
666
+ * the answer is already here before anything can draw. `formats` is the
667
+ * list as objects (`{ id, type, depth, redShift, redMask, ... }`);
668
+ * `byVisual` is a `Map` from visual id to format id.
669
+ *
670
+ * @returns {Promise<{formats: Array<object>, byVisual: Map<number, number>}>}
671
+ */
672
+ pictFormats() {
673
+ if (this._pictFormatsPromise) return this._pictFormatsPromise;
674
+ this._pictFormatsPromise = new Promise((resolve, reject) => {
675
+ const reply = this.display.Render?.pictFormats;
676
+ if (!reply) {
677
+ reject(new Error('ntk: this connection has no RENDER extension, so it has no picture formats'));
678
+ return;
679
+ }
680
+ const formats = parsePictFormats(reply);
681
+ this._pictFormats = {
682
+ formats,
683
+ byVisual: visualFormats(this.display, formats, reply)
684
+ };
685
+ resolve(this._pictFormats);
686
+ });
687
+ return this._pictFormatsPromise;
688
+ }
689
+
690
+ /**
691
+ * The picture format a drawable on `visual` is read and written through.
692
+ *
693
+ * Depth is not enough to name a format and a visual is: a depth-16 visual
694
+ * can be 5:6:5 or 5:5:5, a depth-24 one RGB or BGR, and 10:10:10:2 is 32
695
+ * bits wide like 8:8:8:8. Anything drawing on a drawable it did not create
696
+ * — a compositor holding another client's pixmap, an adopted or embedded
697
+ * window — has to ask, or RENDER reads the channels wrong without ever
698
+ * complaining (issue #295).
699
+ *
700
+ * const attrs = await wnd.getAttributes();
701
+ * const format = await app.pictFormatFor(attrs.visual);
702
+ *
703
+ * `visual` may be an id or a handshake visual object. `depth` is the
704
+ * fallback: where the server names no format for the visual — an indexed
705
+ * visual, or one this connection was never told about — the standard
706
+ * format for that depth is returned, which is what ntk used to assume
707
+ * everywhere.
708
+ *
709
+ * @param {number|object} visual visual id, or `{ vid }`
710
+ * @param {object} [options]
711
+ * @param {number} [options.depth] the drawable's depth, for the fallback
712
+ * @returns {Promise<number|undefined>} picture format id
713
+ */
714
+ async pictFormatFor(visual, { depth } = {}) {
715
+ const id = typeof visual === 'object' && visual ? visual.vid : visual;
716
+ try {
717
+ await this.pictFormats();
718
+ } catch {
719
+ // a server that cannot answer leaves the fallback
720
+ }
721
+ return this._knownPictFormat(id, depth) ?? formatForDepth(this.display.Render, depth);
722
+ }
723
+
724
+ /**
725
+ * The cached answer to `pictFormatFor`, or `undefined` when the table has
726
+ * not arrived, names no format for this visual, or the visual does not
727
+ * describe a drawable of this depth.
728
+ *
729
+ * That last one is the guard against handing RENDER a format it will
730
+ * reject: a picture's format and its drawable's depth have to agree, and
731
+ * the visual on hand is not always the drawable's own — a window's backing
732
+ * pixmap is allocated at a depth ntk sometimes has to assume. Where they
733
+ * disagree the depth is what the drawable really is, so the depth-based
734
+ * format is what it gets.
735
+ *
736
+ * A picture is bound synchronously, in request order with the drawing
737
+ * around it, so the format has to be available without a wait. Callers
738
+ * fall back to the depth and re-bind once `pictFormats()` resolves.
739
+ */
740
+ _knownPictFormat(visual, depth) {
741
+ if (!visual) return undefined;
742
+ const id = visual >>> 0;
743
+ const format = this._pictFormats?.byVisual.get(id);
744
+ if (format === undefined) return undefined;
745
+ // Depth 0 is not a depth: it is CopyFromParent, which ntk never resolved
746
+ // because only the server did — and a window on that visual is of that
747
+ // visual's depth by definition, so there is nothing to disagree with.
748
+ if (depth) {
749
+ this._visualDepths ??= visualDepths(this.display);
750
+ if (this._visualDepths.get(id) !== depth) return undefined;
751
+ }
752
+ return format;
753
+ }
754
+
459
755
  rootWindow(screen = 0) {
460
756
  return new Window(this, { id: this.display.screen[screen].root });
461
757
  }
@@ -497,6 +793,42 @@ export default class App {
497
793
  return new Pixmap(this, args);
498
794
  }
499
795
 
796
+ /**
797
+ * The XFIXES extension for this connection, loaded once and shared.
798
+ *
799
+ * Regions live here — the server-side rectangle sets X uses for damage,
800
+ * window shapes and compositor bookkeeping — along with the requests that
801
+ * combine them and the one that hangs a region on a Picture as its clip.
802
+ *
803
+ * The throwing spelling of `app.xfixes()`: same query, same cache, but a
804
+ * server without XFIXES rejects with code `'ERR_NTK_NO_XFIXES'` instead of
805
+ * resolving `null` — regions cannot degrade, so their absence is an error
806
+ * here rather than an answer.
807
+ *
808
+ * @returns {Promise<object>} node-x11's XFIXES extension object
809
+ */
810
+ async fixes() {
811
+ const ext = await this.xfixes();
812
+ if (!ext) throw noXFixesError();
813
+ this._fixes = ext;
814
+ return ext;
815
+ }
816
+
817
+ /**
818
+ * A server-side region of the given rectangles (`{ x, y, width, height }`
819
+ * or ntk's own `{ x, y, w, h }`), empty by default.
820
+ *
821
+ * Async because XFIXES is loaded on first use; a second region costs one
822
+ * request and no round trip. Free it with `destroy()` — or `using`, or let
823
+ * the GC do it (docs/resource-management.md).
824
+ *
825
+ * @param {Array<object>} [rects]
826
+ * @returns {Promise<Region>}
827
+ */
828
+ async createRegion(rects = []) {
829
+ return new Region(this, await this.fixes(), rects);
830
+ }
831
+
500
832
  /**
501
833
  * A repeating source Picture of one colour, for compositing. Components
502
834
  * are 0..1 floats, premultiplied by alpha.
package/lib/drawable.js CHANGED
@@ -11,6 +11,16 @@ export default class Drawable extends EventEmitter {
11
11
  if (!factory) throw new Error(`Unknown rendering context: ${name}`);
12
12
  return factory(this, ...args);
13
13
  }
14
+
15
+ /**
16
+ * Deliver one named event to this drawable's listeners. Window overrides
17
+ * this with per-frame coalescing and pacing; the base emits directly,
18
+ * for drawables with no frame clock — a Pixmap a DAMAGE object watches
19
+ * (see App#_routeExtensionEvents).
20
+ */
21
+ _deliverEvent(name, ev) {
22
+ this.emit(name, ev);
23
+ }
14
24
  }
15
25
 
16
26
  // populated by the renderingcontext_* modules on import
package/lib/events_map.js CHANGED
@@ -86,6 +86,10 @@ export const coalesce = {
86
86
  mousemove: 'last',
87
87
  resize: 'last',
88
88
  expose: 'union',
89
+ // DamageNotify is the expose case again — a burst of rectangles whose
90
+ // union is what a repaint wants — reported about a drawable's content
91
+ // instead of a window's visibility
92
+ damage: 'union',
89
93
  // 'accumulate' — scroll distance adds up. Keeping the last delta instead
90
94
  // would throw away everything but the final step of a fast scroll, and a
91
95
  // frame's worth of a touchpad's sub-notch deltas is exactly the case the
@@ -93,6 +97,101 @@ export const coalesce = {
93
97
  wheel: 'accumulate'
94
98
  };
95
99
 
100
+ // XFixes SelectionNotify subtype -> why the ownership changed, the same
101
+ // vocabulary clipboard.watch already answers in. The codes are fixed by
102
+ // xfixesproto (SelectionEvent), not assigned by the server.
103
+ const selectionReason = ['new-owner', 'destroyed', 'closed'];
104
+
105
+ // ShapeNotify kind -> which of the window's three shapes changed
106
+ // (shapeproto ShapeKind)
107
+ const shapeKind = ['bounding', 'clip', 'input'];
108
+
109
+ /**
110
+ * The non-generic extension events, and how to deliver them.
111
+ *
112
+ * Unlike core events their type codes are assigned by the server at
113
+ * QueryExtension time (`ext.firstEvent` + a fixed offset), and the drawable
114
+ * each one names arrives under the field its own protocol calls it —
115
+ * DamageNotify a `drawable`, the others a `window` — never under the `wid`
116
+ * node-x11 dispatches per-window consumers by. So they need both halves of
117
+ * this table: a name, and which field to route by. App reads it when an
118
+ * extension is required through its accessors (`app.damage()` and friends)
119
+ * and hands each event to the Window or Pixmap it names — see
120
+ * App#_routeExtensionEvents and docs/app.md "Extension events".
121
+ *
122
+ * Keyed by node-x11's module name, then by the event's key in `ext.events`.
123
+ * `translate` shapes the raw node-x11 event into the one delivered.
124
+ */
125
+ export const extension = {
126
+ damage: {
127
+ DamageNotify: {
128
+ name: 'damage',
129
+ target: 'drawable',
130
+ // expose-shaped, because it is the same news: the box in
131
+ // x/y/width/height so 'union' coalescing applies to it unchanged.
132
+ // Bit 7 of the level byte is the wire's own "more follow" flag —
133
+ // split out, since the report level it rides on is 0..3.
134
+ translate: (ev) => ({
135
+ x: ev.area.x,
136
+ y: ev.area.y,
137
+ width: ev.area.w,
138
+ height: ev.area.h,
139
+ geometry: ev.geometry,
140
+ damage: ev.damage,
141
+ level: ev.level & 0x7f,
142
+ more: !!(ev.level & 0x80),
143
+ time: ev.time
144
+ })
145
+ }
146
+ },
147
+ fixes: {
148
+ // 'selection' is taken — it is core SelectionNotify, a conversion
149
+ // answered — and this event is about who owns the selection, hence the
150
+ // qualified name
151
+ SelectionNotify: {
152
+ name: 'selection_owner',
153
+ target: 'window',
154
+ translate: (ev) => ({
155
+ selection: ev.selection,
156
+ owner: ev.owner,
157
+ reason: selectionReason[ev.subtype] ?? ev.subtype,
158
+ timestamp: ev.timestamp,
159
+ selectionTimestamp: ev.selectionTimestamp
160
+ })
161
+ },
162
+ CursorNotify: {
163
+ name: 'cursor',
164
+ target: 'window',
165
+ translate: (ev) => ({
166
+ cursorSerial: ev.cursorSerial,
167
+ cursorName: ev.cursorName,
168
+ time: ev.timestamp
169
+ })
170
+ }
171
+ },
172
+ shape: {
173
+ ShapeNotify: {
174
+ name: 'shape',
175
+ target: 'window',
176
+ translate: (ev) => ({
177
+ kind: shapeKind[ev.kind] ?? ev.kind,
178
+ x: ev.x,
179
+ y: ev.y,
180
+ width: ev.width,
181
+ height: ev.height,
182
+ shaped: !!ev.shaped,
183
+ time: ev.time
184
+ })
185
+ }
186
+ }
187
+ };
188
+
189
+ // every routed extension event name — what a Pixmap watches `newListener`
190
+ // for to enrol itself in the routing table (see lib/pixmap.js)
191
+ export const extensionEventNames = new Set(
192
+ Object.values(extension).flatMap((events) => Object.values(events).map((spec) => spec.name))
193
+ );
194
+
96
195
  export const toSnake = {
97
196
  onMouseMove: 'mousemove',
98
197
  onMouseOver: 'mouseover',
@@ -124,4 +223,4 @@ export const maskCamelCase = Object.fromEntries(
124
223
  Object.entries(toSnake).map(([camel, snake]) => [camel, mask[snake]])
125
224
  );
126
225
 
127
- export default { eventName, mask, maskCamelCase, toSnake, coalesce };
226
+ export default { eventName, mask, maskCamelCase, toSnake, coalesce, extension, extensionEventNames };