@flighthq/tray 0.3.0-next.906.07cea63 → 0.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.
@@ -1,2 +1,3 @@
1
+ export * from './enableTrayGuards';
1
2
  export * from './tray';
2
3
  //# sourceMappingURL=contract.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC"}
1
+ {"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,QAAQ,CAAC"}
package/dist/contract.js CHANGED
@@ -1,2 +1,3 @@
1
+ export * from './enableTrayGuards';
1
2
  export * from './tray';
2
3
  //# sourceMappingURL=contract.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC"}
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,QAAQ,CAAC"}
@@ -0,0 +1,17 @@
1
+ /** Uninstalls the guard installed by `enableTrayGuards`. */
2
+ export declare function disableTrayGuards(): void;
3
+ /**
4
+ * Installs the caller-facing tray guard (opt-in, dev-only). It warns once — through `@flighthq/log` —
5
+ * when `startTrayIconAnimation` is given a non-positive interval.
6
+ *
7
+ * A `setInterval` of zero or less does not fail; it schedules as fast as the host will run it, writing
8
+ * the tray icon every tick. The animation "works", the app burns a core and hammers the platform's tray
9
+ * API, and nothing points at the call that asked for it. That is caller misuse rather than an expected
10
+ * failure, so it warns rather than throwing or returning a sentinel — the animation still starts, and
11
+ * the developer gets told why their machine is busy.
12
+ *
13
+ * Not importing this module costs production nothing: the message and the `@flighthq/log` dependency
14
+ * live only here.
15
+ */
16
+ export declare function enableTrayGuards(): void;
17
+ //# sourceMappingURL=enableTrayGuards.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enableTrayGuards.d.ts","sourceRoot":"","sources":["../src/enableTrayGuards.ts"],"names":[],"mappings":"AAMA,4DAA4D;AAC5D,wBAAgB,iBAAiB,IAAI,IAAI,CAExC;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC"}
@@ -0,0 +1,32 @@
1
+ import { logOnce } from '@flighthq/log/contract';
2
+ import { LogLevel } from '@flighthq/types/contract';
3
+ import { setTrayAnimationGuard } from './tray';
4
+ /** Uninstalls the guard installed by `enableTrayGuards`. */
5
+ export function disableTrayGuards() {
6
+ setTrayAnimationGuard(null);
7
+ }
8
+ /**
9
+ * Installs the caller-facing tray guard (opt-in, dev-only). It warns once — through `@flighthq/log` —
10
+ * when `startTrayIconAnimation` is given a non-positive interval.
11
+ *
12
+ * A `setInterval` of zero or less does not fail; it schedules as fast as the host will run it, writing
13
+ * the tray icon every tick. The animation "works", the app burns a core and hammers the platform's tray
14
+ * API, and nothing points at the call that asked for it. That is caller misuse rather than an expected
15
+ * failure, so it warns rather than throwing or returning a sentinel — the animation still starts, and
16
+ * the developer gets told why their machine is busy.
17
+ *
18
+ * Not importing this module costs production nothing: the message and the `@flighthq/log` dependency
19
+ * live only here.
20
+ */
21
+ export function enableTrayGuards() {
22
+ setTrayAnimationGuard(warnOnUnboundedTrayAnimation);
23
+ }
24
+ function warnOnUnboundedTrayAnimation(_tray, _frameCount, intervalMs) {
25
+ if (intervalMs > 0)
26
+ return;
27
+ logOnce('tray:non-positive-animation-interval', LogLevel.Warn, {
28
+ message: `startTrayIconAnimation: intervalMs is ${intervalMs}, so the icon will be rewritten as fast as the ` +
29
+ 'host schedules timers rather than on a frame interval. Pass a positive millisecond interval.',
30
+ }, 'tray');
31
+ }
32
+ //# sourceMappingURL=enableTrayGuards.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"enableTrayGuards.js","sourceRoot":"","sources":["../src/enableTrayGuards.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAEjD,OAAO,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AAEpD,OAAO,EAAE,qBAAqB,EAAE,MAAM,QAAQ,CAAC;AAE/C,4DAA4D;AAC5D,MAAM,UAAU,iBAAiB;IAC/B,qBAAqB,CAAC,IAAI,CAAC,CAAC;AAC9B,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,gBAAgB;IAC9B,qBAAqB,CAAC,4BAA4B,CAAC,CAAC;AACtD,CAAC;AAED,SAAS,4BAA4B,CAAC,KAAe,EAAE,WAAmB,EAAE,UAAkB;IAC5F,IAAI,UAAU,GAAG,CAAC;QAAE,OAAO;IAC3B,OAAO,CACL,sCAAsC,EACtC,QAAQ,CAAC,IAAI,EACb;QACE,OAAO,EACL,yCAAyC,UAAU,iDAAiD;YACpG,8FAA8F;KACjG,EACD,MAAM,CACP,CAAC;AACJ,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export { createTrayIcon, destroyTrayIcon, displayTrayBalloon, getTrayCapabilities, getTrayIconBounds, getTrayIconTitle, getTrayIconTooltip, getTrayIcons, isTrayDestroyed, onTrayEvent, popupTrayContextMenu, removeTrayBalloon, setTrayIcon, setTrayIconContextMenu, setTrayIconTemplate, setTrayIconTitle, setTrayIconTooltip, setTrayIgnoreDoubleClickEvents, setTrayPressedIcon, startTrayIconAnimation, } from './contract';
1
+ export { disableTrayGuards, enableTrayGuards, createTrayIcon, destroyTrayIcon, displayTrayBalloon, getTrayCapabilities, getTrayIconBounds, getTrayIconTitle, getTrayIconTooltip, getTrayIcons, isTrayDestroyed, isTrayIconAnimating, onTrayEvent, popupTrayContextMenu, removeTrayBalloon, setTrayIcon, setTrayIconContextMenu, setTrayIconTemplate, setTrayIconTitle, setTrayIconTooltip, setTrayIgnoreDoubleClickEvents, setTrayPressedIcon, startTrayIconAnimation, stopTrayIconAnimation, } from './contract';
2
2
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,EAClB,YAAY,EACZ,eAAe,EACf,WAAW,EACX,oBAAoB,EACpB,iBAAiB,EACjB,WAAW,EACX,sBAAsB,EACtB,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,8BAA8B,EAC9B,kBAAkB,EAClB,sBAAsB,GACvB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,EAClB,YAAY,EACZ,eAAe,EACf,mBAAmB,EACnB,WAAW,EACX,oBAAoB,EACpB,iBAAiB,EACjB,WAAW,EACX,sBAAsB,EACtB,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,8BAA8B,EAC9B,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { createTrayIcon, destroyTrayIcon, displayTrayBalloon, getTrayCapabilities, getTrayIconBounds, getTrayIconTitle, getTrayIconTooltip, getTrayIcons, isTrayDestroyed, onTrayEvent, popupTrayContextMenu, removeTrayBalloon, setTrayIcon, setTrayIconContextMenu, setTrayIconTemplate, setTrayIconTitle, setTrayIconTooltip, setTrayIgnoreDoubleClickEvents, setTrayPressedIcon, startTrayIconAnimation, } from './contract';
1
+ export { disableTrayGuards, enableTrayGuards, createTrayIcon, destroyTrayIcon, displayTrayBalloon, getTrayCapabilities, getTrayIconBounds, getTrayIconTitle, getTrayIconTooltip, getTrayIcons, isTrayDestroyed, isTrayIconAnimating, onTrayEvent, popupTrayContextMenu, removeTrayBalloon, setTrayIcon, setTrayIconContextMenu, setTrayIconTemplate, setTrayIconTitle, setTrayIconTooltip, setTrayIgnoreDoubleClickEvents, setTrayPressedIcon, startTrayIconAnimation, stopTrayIconAnimation, } from './contract';
2
2
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,EAClB,YAAY,EACZ,eAAe,EACf,WAAW,EACX,oBAAoB,EACpB,iBAAiB,EACjB,WAAW,EACX,sBAAsB,EACtB,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,8BAA8B,EAC9B,kBAAkB,EAClB,sBAAsB,GACvB,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,kBAAkB,EAClB,YAAY,EACZ,eAAe,EACf,mBAAmB,EACnB,WAAW,EACX,oBAAoB,EACpB,iBAAiB,EACjB,WAAW,EACX,sBAAsB,EACtB,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,8BAA8B,EAC9B,kBAAkB,EAClB,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,YAAY,CAAC"}
package/dist/tray.d.ts CHANGED
@@ -11,9 +11,14 @@ export declare function getTrayIcons(): readonly TrayIcon[];
11
11
  export declare function getTrayIconTitle(tray: TrayIcon): string;
12
12
  export declare function getTrayIconTooltip(tray: TrayIcon): string;
13
13
  export declare function isTrayDestroyed(tray: TrayIcon): boolean;
14
+ export declare function isTrayIconAnimating(tray: TrayIcon): boolean;
14
15
  export declare function onTrayEvent(listener: (event: Readonly<TrayEventData>) => void): () => void;
15
16
  export declare function popupTrayContextMenu(tray: TrayIcon, position?: Readonly<Vector2Like>): void;
16
17
  export declare function removeTrayBalloon(tray: TrayIcon): void;
18
+ /** Installs the tray guard, or clears it with `null`. The seam exists so the messages and the
19
+ * `@flighthq/log` dependency live in the separately-importable guard module rather than here; not
20
+ * importing that module costs production nothing. Called by `enableTrayGuards`, not directly. */
21
+ export declare function setTrayAnimationGuard(guard: ((tray: TrayIcon, frameCount: number, intervalMs: number) => void) | null): void;
17
22
  export declare function setTrayBackend(backend: TrayBackend | null): void;
18
23
  export declare function setTrayIcon(tray: TrayIcon, icon: string): void;
19
24
  export declare function setTrayIconContextMenu(tray: TrayIcon, items: readonly MenuItemTemplate[]): void;
@@ -23,4 +28,5 @@ export declare function setTrayIconTooltip(tray: TrayIcon, tooltip: string): voi
23
28
  export declare function setTrayIgnoreDoubleClickEvents(tray: TrayIcon, ignore: boolean): void;
24
29
  export declare function setTrayPressedIcon(tray: TrayIcon, icon: string): void;
25
30
  export declare function startTrayIconAnimation(tray: TrayIcon, frames: readonly string[], intervalMs: number): () => void;
31
+ export declare function stopTrayIconAnimation(tray: TrayIcon): void;
26
32
  //# sourceMappingURL=tray.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"tray.d.ts","sourceRoot":"","sources":["../src/tray.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,gBAAgB,EAChB,aAAa,EACb,WAAW,EACX,kBAAkB,EAClB,gBAAgB,EAChB,aAAa,EACb,QAAQ,EACR,eAAe,EAChB,MAAM,0BAA0B,CAAC;AAClC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAc5D,wBAAgB,cAAc,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,eAAe,CAAC,GAAG,QAAQ,GAAG,IAAI,CAGnF;AAKD,wBAAgB,oBAAoB,IAAI,WAAW,CAmElD;AAGD,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAEpD;AAID,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,CAAC,kBAAkB,CAAC,GAAG,IAAI,CAE9F;AAGD,wBAAgB,cAAc,IAAI,WAAW,CAG5C;AAKD,wBAAgB,mBAAmB,IAAI,QAAQ,CAAC,gBAAgB,CAAC,CAEhE;AAID,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,GAAG,IAAI,CAEhF;AAGD,wBAAgB,YAAY,IAAI,SAAS,QAAQ,EAAE,CAIlD;AAGD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAEvD;AAGD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAEzD;AAID,wBAAgB,eAAe,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAEvD;AAKD,wBAAgB,WAAW,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,QAAQ,CAAC,aAAa,CAAC,KAAK,IAAI,GAAG,MAAM,IAAI,CAE1F;AAKD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,QAAQ,EAAE,QAAQ,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC,GAAG,IAAI,CAE3F;AAGD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,QAAQ,GAAG,IAAI,CAEtD;AAGD,wBAAgB,cAAc,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,GAAG,IAAI,CAEhE;AAID,wBAAgB,WAAW,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAE9D;AAID,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,gBAAgB,EAAE,GAAG,IAAI,CAE/F;AAKD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,EAAE,UAAU,EAAE,OAAO,GAAG,IAAI,CAE7E;AAGD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAEpE;AAGD,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAExE;AAID,wBAAgB,8BAA8B,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,GAAG,IAAI,CAEpF;AAID,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAErE;AAOD,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,MAAM,EAAE,EAAE,UAAU,EAAE,MAAM,GAAG,MAAM,IAAI,CAShH"}
1
+ {"version":3,"file":"tray.d.ts","sourceRoot":"","sources":["../src/tray.ts"],"names":[],"mappings":"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"}
package/dist/tray.js CHANGED
@@ -1,4 +1,6 @@
1
- // Web tray capability constants. Web has no system tray — all capabilities are false.
1
+ // Web tray capability constants. Web has no system tray — all capabilities are false. Readonly and
2
+ // shared: getCapabilities hands this same object to every caller, so a mutable one would let any of
3
+ // them rewrite what the backend reports to all the others.
2
4
  const WEB_CAPABILITIES = {
3
5
  balloon: false,
4
6
  bounds: false,
@@ -85,7 +87,12 @@ export function createWebTrayBackend() {
85
87
  };
86
88
  }
87
89
  // Destroys a tray icon and frees its host resource. No-op when the host has no tray.
90
+ // Any icon animation running on this tray is stopped first: the interval writes through setTrayIcon,
91
+ // and left running it would keep calling into a resource the host has freed — forever, since the
92
+ // timer holds its own closure alive. Destroying the thing being animated is the clearest possible
93
+ // signal that the animation is over, so the caller does not have to have kept the stop function.
88
94
  export function destroyTrayIcon(tray) {
95
+ stopTrayIconAnimation(tray);
89
96
  getTrayBackend().destroy(tray.id);
90
97
  }
91
98
  // Displays a Windows balloon notification from the tray icon. No-op on macOS/Linux and on web.
@@ -129,6 +136,11 @@ export function getTrayIconTooltip(tray) {
129
136
  export function isTrayDestroyed(tray) {
130
137
  return getTrayBackend().isDestroyed(tray.id);
131
138
  }
139
+ // True while an icon animation started by startTrayIconAnimation is running on this tray. False once
140
+ // it is stopped, replaced by a later start, or the tray is destroyed.
141
+ export function isTrayIconAnimating(tray) {
142
+ return _animations.has(tray.id);
143
+ }
132
144
  // Subscribes to tray icon events, delivering a rich TrayEventData payload (id, type, bounds,
133
145
  // position, modifier keys, and drop payloads). Returns an unsubscribe function. On web this
134
146
  // never fires (no tray); a native host is required.
@@ -145,6 +157,12 @@ export function popupTrayContextMenu(tray, position) {
145
157
  export function removeTrayBalloon(tray) {
146
158
  getTrayBackend().removeBalloon(tray.id);
147
159
  }
160
+ /** Installs the tray guard, or clears it with `null`. The seam exists so the messages and the
161
+ * `@flighthq/log` dependency live in the separately-importable guard module rather than here; not
162
+ * importing that module costs production nothing. Called by `enableTrayGuards`, not directly. */
163
+ export function setTrayAnimationGuard(guard) {
164
+ _animationGuard = guard;
165
+ }
148
166
  // Installs a native host tray backend; pass null to fall back to the web default.
149
167
  export function setTrayBackend(backend) {
150
168
  _backend = backend;
@@ -183,21 +201,53 @@ export function setTrayIgnoreDoubleClickEvents(tray, ignore) {
183
201
  export function setTrayPressedIcon(tray, icon) {
184
202
  getTrayBackend().setPressedIcon(tray.id, icon);
185
203
  }
186
- // Starts an animated icon sequence by cycling through the given frames at the specified interval.
187
- // The caller owns the timer this function is a thin helper over setTrayIcon that starts an
188
- // interval and returns a stop function. Call the returned function (or stopTrayIconAnimation) to
189
- // cancel. The tray icon is not destroyed when the animation stops.
204
+ // Starts an animated icon sequence by cycling through the given frames at the specified interval,
205
+ // returning a stop function. An empty frame list is a no-op. The tray icon is not destroyed when the
206
+ // animation stops, and the last frame shown stays.
207
+ //
208
+ // A tray icon has one image, so it has one animation: starting a second on the same tray replaces the
209
+ // first rather than running both. Without that, a perfectly ordinary sequence — swap a "syncing"
210
+ // animation for an "error" one — left two intervals writing to the same icon on the same tick, and
211
+ // the only handle to the orphaned one was a stop function the caller had no reason to still be
212
+ // holding. The returned function stops only the animation it started, so calling a stale one cannot
213
+ // cancel a newer animation.
214
+ //
190
215
  // Note: interval timing is best-effort; the actual frame rate depends on the host event loop.
191
216
  export function startTrayIconAnimation(tray, frames, intervalMs) {
192
217
  if (frames.length === 0)
193
- return () => { };
218
+ return _noopStop;
219
+ _animationGuard?.(tray, frames.length, intervalMs);
220
+ stopTrayIconAnimation(tray);
194
221
  let index = 0;
195
222
  setTrayIcon(tray, frames[index]);
196
223
  const handle = setInterval(() => {
197
224
  index = (index + 1) % frames.length;
198
225
  setTrayIcon(tray, frames[index]);
199
226
  }, intervalMs);
200
- return () => clearInterval(handle);
227
+ _animations.set(tray.id, handle);
228
+ return () => {
229
+ // Only clear if this animation is still the current one; a later start already replaced it.
230
+ if (_animations.get(tray.id) !== handle)
231
+ return;
232
+ clearInterval(handle);
233
+ _animations.delete(tray.id);
234
+ };
201
235
  }
202
236
  let _backend = null;
237
+ // The interval per animating tray id. Module-scoped like the backend, and correct at that scope: an
238
+ // id is the host's, so one id is one icon is one animation.
239
+ const _animations = new Map();
240
+ function _noopStop() { }
241
+ // Stops the icon animation running on this tray, if any. Idempotent, and safe on a tray that never
242
+ // animated. The named counterpart to startTrayIconAnimation for callers that hold the TrayIcon but
243
+ // not the stop function it returned — which is most of them, since the handle is what gets passed
244
+ // around and the closure is what gets dropped.
245
+ export function stopTrayIconAnimation(tray) {
246
+ const handle = _animations.get(tray.id);
247
+ if (handle === undefined)
248
+ return;
249
+ clearInterval(handle);
250
+ _animations.delete(tray.id);
251
+ }
252
+ let _animationGuard = null;
203
253
  //# sourceMappingURL=tray.js.map
package/dist/tray.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"tray.js","sourceRoot":"","sources":["../src/tray.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tray.js","sourceRoot":"","sources":["../src/tray.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,8 @@
1
1
  {
2
2
  "name": "@flighthq/tray",
3
- "version": "0.3.0-next.906.07cea63",
3
+ "version": "0.3.0",
4
+ "author": "Joshua Granick and other contributors",
5
+ "license": "MIT",
4
6
  "repository": {
5
7
  "type": "git",
6
8
  "url": "https://github.com/flighthq/flight.git",
@@ -36,7 +38,8 @@
36
38
  "clean:dist": "tsx ../../scripts/clean-package-dist.ts"
37
39
  },
38
40
  "dependencies": {
39
- "@flighthq/types": "0.3.0-next.906.07cea63"
41
+ "@flighthq/log": "0.3.0",
42
+ "@flighthq/types": "0.3.0"
40
43
  },
41
44
  "devDependencies": {
42
45
  "typescript": "^5.3.0"
@@ -0,0 +1,79 @@
1
+ import { setLogSink } from '@flighthq/log/contract';
2
+ import type { LogEntry } from '@flighthq/types/contract';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import { disableTrayGuards, enableTrayGuards } from './enableTrayGuards';
6
+ import { createTrayIcon, setTrayBackend, startTrayIconAnimation, stopTrayIconAnimation } from './tray';
7
+
8
+ let entries: LogEntry[];
9
+
10
+ beforeEach(() => {
11
+ vi.useFakeTimers();
12
+ entries = [];
13
+ setLogSink((entry) => entries.push(entry));
14
+ setTrayBackend({
15
+ create: () => 1,
16
+ destroy: () => {},
17
+ getBounds: () => null,
18
+ getCapabilities: () => ({ animation: true, badge: false, bounds: true, contextMenu: true, title: true }),
19
+ getTitle: () => '',
20
+ isDestroyed: () => false,
21
+ listIds: () => [1],
22
+ setContextMenu: () => {},
23
+ setIcon: () => {},
24
+ setPressedIcon: () => {},
25
+ setTitle: () => {},
26
+ setTooltip: () => {},
27
+ subscribe: () => () => {},
28
+ } as never);
29
+ });
30
+
31
+ afterEach(() => {
32
+ disableTrayGuards();
33
+ setTrayBackend(null);
34
+ setLogSink(null);
35
+ vi.useRealTimers();
36
+ });
37
+
38
+ function messages(): string {
39
+ return entries.map((e) => String((e.data as { message?: unknown } | undefined)?.message ?? '')).join('\n');
40
+ }
41
+
42
+ describe('disableTrayGuards', () => {
43
+ it('stops the guard inspecting later animations', () => {
44
+ enableTrayGuards();
45
+ disableTrayGuards();
46
+ const tray = createTrayIcon()!;
47
+ startTrayIconAnimation(tray, ['a', 'b'], 0);
48
+ stopTrayIconAnimation(tray);
49
+ expect(messages()).toBe('');
50
+ });
51
+ });
52
+
53
+ describe('enableTrayGuards', () => {
54
+ it('says nothing for a positive interval', () => {
55
+ enableTrayGuards();
56
+ const tray = createTrayIcon()!;
57
+ startTrayIconAnimation(tray, ['a', 'b'], 100);
58
+ stopTrayIconAnimation(tray);
59
+ expect(messages()).toBe('');
60
+ });
61
+
62
+ // A non-positive interval does not fail — setInterval simply schedules as fast as the host will run
63
+ // it, so the animation "works" while burning a core. Nothing points at the call that asked for it.
64
+ it('warns when the interval is zero, and still starts the animation', () => {
65
+ enableTrayGuards();
66
+ const tray = createTrayIcon()!;
67
+
68
+ const stop = startTrayIconAnimation(tray, ['a', 'b'], 0);
69
+
70
+ expect(messages()).toContain('intervalMs is 0');
71
+ // The wording that explains WHY is asserted here rather than in a separate negative-interval test:
72
+ // logOnce suppresses a key for the process, so a second test tripping this key would pass or fail
73
+ // on file order alone.
74
+ expect(messages()).toContain('as fast as the host schedules');
75
+ // Warned, not refused: this is misuse, not an expected failure.
76
+ expect(typeof stop).toBe('function');
77
+ stop();
78
+ });
79
+ });
package/src/tray.test.ts CHANGED
@@ -20,9 +20,11 @@ import {
20
20
  getTrayIconTitle,
21
21
  getTrayIconTooltip,
22
22
  isTrayDestroyed,
23
+ isTrayIconAnimating,
23
24
  onTrayEvent,
24
25
  popupTrayContextMenu,
25
26
  removeTrayBalloon,
27
+ setTrayAnimationGuard,
26
28
  setTrayBackend,
27
29
  setTrayIcon,
28
30
  setTrayIconContextMenu,
@@ -32,6 +34,7 @@ import {
32
34
  setTrayIgnoreDoubleClickEvents,
33
35
  setTrayPressedIcon,
34
36
  startTrayIconAnimation,
37
+ stopTrayIconAnimation,
35
38
  } from './tray';
36
39
 
37
40
  // Fake backend state per icon.
@@ -54,6 +57,9 @@ function fakeBackend(caps: Partial<TrayCapabilities> = {}): TrayBackend & {
54
57
  capabilities: TrayCapabilities;
55
58
  fireEvent(event: TrayEventData): void;
56
59
  lastPopupPosition: { x: number; y: number } | null;
60
+ // Every icon written, in order — the animation tests assert on the sequence of writes rather than
61
+ // the final image, because two competing animations can leave a correct-looking final frame.
62
+ setIconCalls: string[];
57
63
  trays: Map<number, FakeTray>;
58
64
  } {
59
65
  const capabilities: TrayCapabilities = {
@@ -68,9 +74,12 @@ function fakeBackend(caps: Partial<TrayCapabilities> = {}): TrayBackend & {
68
74
  let eventListener: ((event: Readonly<TrayEventData>) => void) | null = null;
69
75
  const trays = new Map<number, FakeTray>();
70
76
 
77
+ const setIconCalls: string[] = [];
78
+
71
79
  return {
72
80
  capabilities,
73
81
  trays,
82
+ setIconCalls,
74
83
  lastPopupPosition: null,
75
84
  create(options) {
76
85
  const id = nextId++;
@@ -129,6 +138,7 @@ function fakeBackend(caps: Partial<TrayCapabilities> = {}): TrayBackend & {
129
138
  if (tray) tray.contextMenu = items;
130
139
  },
131
140
  setIcon(id, icon) {
141
+ setIconCalls.push(icon);
132
142
  const tray = trays.get(id);
133
143
  if (tray) tray.icon = icon;
134
144
  },
@@ -396,6 +406,43 @@ describe('isTrayDestroyed', () => {
396
406
  });
397
407
  });
398
408
 
409
+ describe('isTrayIconAnimating', () => {
410
+ beforeEach(() => {
411
+ vi.useFakeTimers();
412
+ });
413
+
414
+ afterEach(() => {
415
+ vi.useRealTimers();
416
+ });
417
+
418
+ it('is false before, true during, and false after an animation', () => {
419
+ setTrayBackend(fakeBackend());
420
+ const tray = createTrayIcon({ icon: 'a.png' })!;
421
+ expect(isTrayIconAnimating(tray)).toBe(false);
422
+ const stop = startTrayIconAnimation(tray, ['a.png', 'b.png'], 100);
423
+ expect(isTrayIconAnimating(tray)).toBe(true);
424
+ stop();
425
+ expect(isTrayIconAnimating(tray)).toBe(false);
426
+ });
427
+
428
+ it('is false for an empty frame list, which starts nothing', () => {
429
+ setTrayBackend(fakeBackend());
430
+ const tray = createTrayIcon({ icon: 'a.png' })!;
431
+ startTrayIconAnimation(tray, [], 100);
432
+ expect(isTrayIconAnimating(tray)).toBe(false);
433
+ });
434
+
435
+ it('tracks each tray independently', () => {
436
+ setTrayBackend(fakeBackend());
437
+ const first = createTrayIcon({ icon: 'a.png' })!;
438
+ const second = createTrayIcon({ icon: 'b.png' })!;
439
+ startTrayIconAnimation(first, ['a.png', 'b.png'], 100);
440
+ expect(isTrayIconAnimating(first)).toBe(true);
441
+ expect(isTrayIconAnimating(second)).toBe(false);
442
+ stopTrayIconAnimation(first);
443
+ });
444
+ });
445
+
399
446
  describe('onTrayEvent', () => {
400
447
  it('delivers rich TrayEventData via the active backend', () => {
401
448
  const backend = fakeBackend();
@@ -494,6 +541,42 @@ describe('removeTrayBalloon', () => {
494
541
  });
495
542
  });
496
543
 
544
+ describe('setTrayAnimationGuard', () => {
545
+ afterEach(() => {
546
+ setTrayAnimationGuard(null);
547
+ setTrayBackend(null);
548
+ });
549
+
550
+ // The seam keeps the message and the @flighthq/log dependency in the separately-importable guard
551
+ // module; startTrayIconAnimation only calls whatever is installed.
552
+ it('reports the frame count and interval for each animation start', () => {
553
+ setTrayBackend(fakeBackend());
554
+ const seen: Array<[number, number]> = [];
555
+ setTrayAnimationGuard((_tray, frameCount, intervalMs) => seen.push([frameCount, intervalMs]));
556
+ const tray = createTrayIcon()!;
557
+
558
+ stopTrayIconAnimation(tray);
559
+ startTrayIconAnimation(tray, ['a', 'b', 'c'], 0);
560
+ stopTrayIconAnimation(tray);
561
+
562
+ expect(seen).toEqual([[3, 0]]);
563
+ });
564
+
565
+ it('stops reporting once cleared with null', () => {
566
+ setTrayBackend(fakeBackend());
567
+ let calls = 0;
568
+ setTrayAnimationGuard(() => (calls += 1));
569
+ const tray = createTrayIcon()!;
570
+ startTrayIconAnimation(tray, ['a'], 5);
571
+ stopTrayIconAnimation(tray);
572
+ setTrayAnimationGuard(null);
573
+ startTrayIconAnimation(tray, ['a'], 5);
574
+ stopTrayIconAnimation(tray);
575
+
576
+ expect(calls).toBe(1);
577
+ });
578
+ });
579
+
497
580
  describe('setTrayBackend', () => {
498
581
  it('clears back to the web fallback when passed null', () => {
499
582
  setTrayBackend(fakeBackend());
@@ -645,3 +728,104 @@ describe('startTrayIconAnimation', () => {
645
728
  expect(() => stop()).not.toThrow();
646
729
  });
647
730
  });
731
+
732
+ describe('stopTrayIconAnimation', () => {
733
+ beforeEach(() => {
734
+ vi.useFakeTimers();
735
+ });
736
+
737
+ afterEach(() => {
738
+ vi.useRealTimers();
739
+ });
740
+
741
+ it('stops the interval so no further frames are written', () => {
742
+ const backend = fakeBackend();
743
+ setTrayBackend(backend);
744
+ const tray = createTrayIcon({ icon: 'frame0.png' })!;
745
+ startTrayIconAnimation(tray, ['frame0.png', 'frame1.png'], 100);
746
+ stopTrayIconAnimation(tray);
747
+ const iconAtStop = backend.trays.get(tray.id)!.icon;
748
+ vi.advanceTimersByTime(500);
749
+ expect(backend.trays.get(tray.id)!.icon).toBe(iconAtStop);
750
+ });
751
+
752
+ it('is idempotent and safe on a tray that never animated', () => {
753
+ setTrayBackend(fakeBackend());
754
+ const tray = createTrayIcon({ icon: 'a.png' })!;
755
+ expect(() => {
756
+ stopTrayIconAnimation(tray);
757
+ stopTrayIconAnimation(tray);
758
+ }).not.toThrow();
759
+ });
760
+ });
761
+
762
+ describe('tray icon animation lifecycle', () => {
763
+ beforeEach(() => {
764
+ vi.useFakeTimers();
765
+ });
766
+
767
+ afterEach(() => {
768
+ vi.useRealTimers();
769
+ });
770
+
771
+ it('replaces a running animation instead of running both', () => {
772
+ // The regression this pins: two starts on one tray left two intervals writing to the same icon on
773
+ // the same tick — the icon flickered between the two sequences, and the only handle to the first
774
+ // was a stop function the caller had no reason to still hold.
775
+ const backend = fakeBackend();
776
+ setTrayBackend(backend);
777
+ const tray = createTrayIcon({ icon: 'a0.png' })!;
778
+ startTrayIconAnimation(tray, ['a0.png', 'a1.png'], 100);
779
+ startTrayIconAnimation(tray, ['b0.png', 'b1.png'], 100);
780
+ backend.setIconCalls.length = 0;
781
+ vi.advanceTimersByTime(100);
782
+ expect(backend.setIconCalls).toEqual(['b1.png']);
783
+ });
784
+
785
+ it('stops animating a tray that has been destroyed', () => {
786
+ // The regression this pins: the interval outlived the icon, calling setIcon on a destroyed tray
787
+ // forever — and the timer kept its own closure alive, so nothing could collect it.
788
+ const backend = fakeBackend();
789
+ setTrayBackend(backend);
790
+ const tray = createTrayIcon({ icon: 'a0.png' })!;
791
+ startTrayIconAnimation(tray, ['a0.png', 'a1.png'], 100);
792
+ destroyTrayIcon(tray);
793
+ backend.setIconCalls.length = 0;
794
+ vi.advanceTimersByTime(500);
795
+ expect(backend.setIconCalls).toEqual([]);
796
+ expect(isTrayIconAnimating(tray)).toBe(false);
797
+ });
798
+
799
+ it('a stale stop function cannot cancel the animation that replaced it', () => {
800
+ const backend = fakeBackend();
801
+ setTrayBackend(backend);
802
+ const tray = createTrayIcon({ icon: 'a0.png' })!;
803
+ const staleStop = startTrayIconAnimation(tray, ['a0.png', 'a1.png'], 100);
804
+ startTrayIconAnimation(tray, ['b0.png', 'b1.png'], 100);
805
+ staleStop();
806
+ expect(isTrayIconAnimating(tray)).toBe(true);
807
+ backend.setIconCalls.length = 0;
808
+ vi.advanceTimersByTime(100);
809
+ expect(backend.setIconCalls).toEqual(['b1.png']);
810
+ stopTrayIconAnimation(tray);
811
+ });
812
+ });
813
+
814
+ describe('TrayIcon handle identity', () => {
815
+ afterEach(() => setTrayBackend(null));
816
+
817
+ // Documented contract: handles compare by id, never by identity. Pinned so the doc and the behaviour
818
+ // cannot drift apart, and so anyone who later interns handles has to change this deliberately rather
819
+ // than discover it. The working form is the id comparison, and it is asserted alongside the failing
820
+ // one so the test shows the remedy, not just the trap.
821
+ it('mints a distinct object per call, so handles compare by id rather than identity', () => {
822
+ const backend = fakeBackend();
823
+ setTrayBackend(backend);
824
+ const tray = createTrayIcon()!;
825
+
826
+ const listed = getTrayIcons();
827
+
828
+ expect(listed.includes(tray)).toBe(false);
829
+ expect(listed.some((candidate) => candidate.id === tray.id)).toBe(true);
830
+ });
831
+ });