@flighthq/lifecycle 0.5.1-edge.1059.c56d3cd → 0.5.1-edge.1350.8ce488b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/contract.d.ts +1 -1
- package/dist/contract.d.ts.map +1 -1
- package/dist/contract.js +1 -1
- package/dist/contract.js.map +1 -1
- package/dist/lifecycle.d.ts +9 -11
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +28 -137
- package/dist/lifecycle.js.map +1 -1
- package/package.json +4 -4
- package/src/lifecycle.test.ts +45 -134
- package/src/lifecycleHost.test.ts +22 -16
package/README.md
CHANGED
|
@@ -13,5 +13,5 @@ Import the supported application-facing API from `@flighthq/lifecycle`. The `@fl
|
|
|
13
13
|
This package is part of the locked-version Flight SDK graph. Applications may instead install and import `@flighthq/sdk` when package-level tree shaking is sufficient.
|
|
14
14
|
|
|
15
15
|
- [Flight project](https://github.com/flighthq/flight)
|
|
16
|
-
- [Source for @flighthq/lifecycle@0.5.1-edge.
|
|
17
|
-
- [License](https://github.com/flighthq/flight/blob/
|
|
16
|
+
- [Source for @flighthq/lifecycle@0.5.1-edge.1350.8ce488b](https://github.com/flighthq/flight/tree/8ce488bfe444eded2fb281e7b9696e5ce7fabdc0/packages/lifecycle)
|
|
17
|
+
- [License](https://github.com/flighthq/flight/blob/8ce488bfe444eded2fb281e7b9696e5ce7fabdc0/LICENSE.md)
|
package/dist/contract.d.ts
CHANGED
package/dist/contract.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,sBAAsB,EAAE,
|
|
1
|
+
{"version":3,"file":"contract.d.ts","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/contract.js
CHANGED
package/dist/contract.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,sBAAsB,EAAE,
|
|
1
|
+
{"version":3,"file":"contract.js","sourceRoot":"","sources":["../src/contract.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,sBAAsB,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/lifecycle.d.ts
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import type { BackendOperationExplanation, LifecycleOperation, EntityConstruction } from '@flighthq/types/contract';
|
|
2
|
-
import type { AppLaunchKind,
|
|
3
|
-
export declare function attachAppLifecycle(
|
|
2
|
+
import type { AppLaunchKind, HostLifecycleProvider, AppLifecycle, AppLifecycleState } from '@flighthq/types/contract';
|
|
3
|
+
export declare function attachAppLifecycle(hostLifecycle: Readonly<HostLifecycleProvider>, app: AppLifecycle): void;
|
|
4
4
|
export declare function createAppLifecycle(): AppLifecycle;
|
|
5
|
-
export declare function createWebLifecycleBackend(): LifecycleBackend;
|
|
6
5
|
export declare function detachAppLifecycle(app: AppLifecycle): void;
|
|
7
6
|
export declare function disposeAppLifecycle(app: AppLifecycle): void;
|
|
8
|
-
export declare function explainLifecycleOperation(
|
|
9
|
-
export declare function getAppLaunchKind(
|
|
10
|
-
export declare function getAppLifecycleState(
|
|
11
|
-
export declare function hasLifecycleOperation(
|
|
7
|
+
export declare function explainLifecycleOperation(hostLifecycle: Readonly<HostLifecycleProvider>, operation: LifecycleOperation): BackendOperationExplanation;
|
|
8
|
+
export declare function getAppLaunchKind(hostLifecycle: Readonly<HostLifecycleProvider>): AppLaunchKind;
|
|
9
|
+
export declare function getAppLifecycleState(hostLifecycle: Readonly<HostLifecycleProvider>): AppLifecycleState;
|
|
10
|
+
export declare function hasLifecycleOperation(hostLifecycle: Readonly<HostLifecycleProvider>, operation: LifecycleOperation): boolean;
|
|
12
11
|
export declare function initializeAppLifecycle(out: EntityConstruction<AppLifecycle>): void;
|
|
13
|
-
export declare function
|
|
14
|
-
export declare function
|
|
15
|
-
export declare function
|
|
16
|
-
export declare function isAppInactive(host: HasSystemLifecycle): boolean;
|
|
12
|
+
export declare function isAppActive(hostLifecycle: Readonly<HostLifecycleProvider>): boolean;
|
|
13
|
+
export declare function isAppBackground(hostLifecycle: Readonly<HostLifecycleProvider>): boolean;
|
|
14
|
+
export declare function isAppInactive(hostLifecycle: Readonly<HostLifecycleProvider>): boolean;
|
|
17
15
|
export declare function requestAppBack(app: AppLifecycle): boolean;
|
|
18
16
|
//# sourceMappingURL=lifecycle.d.ts.map
|
package/dist/lifecycle.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/lifecycle.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,2BAA2B,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACpH,OAAO,KAAK,EACV,aAAa,EACb,
|
|
1
|
+
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/lifecycle.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,2BAA2B,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AACpH,OAAO,KAAK,EACV,aAAa,EACb,qBAAqB,EACrB,YAAY,EACZ,iBAAiB,EAElB,MAAM,0BAA0B,CAAC;AAYlC,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,QAAQ,CAAC,qBAAqB,CAAC,EAAE,GAAG,EAAE,YAAY,GAAG,IAAI,CAqC1G;AAED,wBAAgB,kBAAkB,IAAI,YAAY,CAIjD;AAGD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAM1D;AAID,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAG3D;AAID,wBAAgB,yBAAyB,CACvC,aAAa,EAAE,QAAQ,CAAC,qBAAqB,CAAC,EAC9C,SAAS,EAAE,kBAAkB,GAC5B,2BAA2B,CAG7B;AAMD,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,QAAQ,CAAC,qBAAqB,CAAC,GAAG,aAAa,CAG9F;AAGD,wBAAgB,oBAAoB,CAAC,aAAa,EAAE,QAAQ,CAAC,qBAAqB,CAAC,GAAG,iBAAiB,CAEtG;AAGD,wBAAgB,qBAAqB,CACnC,aAAa,EAAE,QAAQ,CAAC,qBAAqB,CAAC,EAC9C,SAAS,EAAE,kBAAkB,GAC5B,OAAO,CAET;AAGD,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,kBAAkB,CAAC,YAAY,CAAC,GAAG,IAAI,CAQlF;AAGD,wBAAgB,WAAW,CAAC,aAAa,EAAE,QAAQ,CAAC,qBAAqB,CAAC,GAAG,OAAO,CAEnF;AAGD,wBAAgB,eAAe,CAAC,aAAa,EAAE,QAAQ,CAAC,qBAAqB,CAAC,GAAG,OAAO,CAEvF;AAID,wBAAgB,aAAa,CAAC,aAAa,EAAE,QAAQ,CAAC,qBAAqB,CAAC,GAAG,OAAO,CAErF;AAOD,wBAAgB,cAAc,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAGzD"}
|
package/dist/lifecycle.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
import { allocateEntity, finishEntity } from '@flighthq/entity/contract';
|
|
2
2
|
import { createSignal, emitSignal } from '@flighthq/signals/contract';
|
|
3
|
-
// Begins delivering lifecycle changes to `app`'s signals by subscribing to the
|
|
3
|
+
// Begins delivering lifecycle changes to `app`'s signals by subscribing to the supplied provider. On
|
|
4
4
|
// each change it reads the current state and emits onStateChange plus onResume when transitioning
|
|
5
|
-
// to 'active' and onPause when leaving 'active'. The
|
|
6
|
-
// environments that support it; native hosts emit it through their own
|
|
5
|
+
// to 'active' and onPause when leaving 'active'. The Web provider drives onBackButton only in
|
|
6
|
+
// environments that support it; native hosts emit it through their own provider. Idempotent: a prior
|
|
7
7
|
// subscription is torn down first. Pair with detachAppLifecycle/disposeAppLifecycle.
|
|
8
8
|
//
|
|
9
|
-
// onStateChange is "raw, not deduped" — it fires on every
|
|
9
|
+
// onStateChange is "raw, not deduped" — it fires on every provider notification regardless of
|
|
10
10
|
// whether the derived state changed. onResume/onPause are deduped edges: 'active'→non-'active'
|
|
11
11
|
// fires onPause (including the interruption edge 'active'→'inactive'); non-'active'→'active' fires
|
|
12
12
|
// onResume. The 'inactive'→'background' and reverse transitions do not fire onPause/onResume again.
|
|
13
|
-
export function attachAppLifecycle(
|
|
13
|
+
export function attachAppLifecycle(hostLifecycle, app) {
|
|
14
14
|
detachAppLifecycle(app);
|
|
15
|
-
const backend =
|
|
15
|
+
const backend = hostLifecycle;
|
|
16
16
|
let previous = backend.getState();
|
|
17
17
|
const unsubscribeState = backend.subscribe(() => {
|
|
18
18
|
const state = backend.getState();
|
|
@@ -51,11 +51,6 @@ export function createAppLifecycle() {
|
|
|
51
51
|
initializeAppLifecycle(out);
|
|
52
52
|
return finishEntity(out);
|
|
53
53
|
}
|
|
54
|
-
export function createWebLifecycleBackend() {
|
|
55
|
-
const out = allocateEntity();
|
|
56
|
-
initializeWebLifecycleBackend(out);
|
|
57
|
-
return finishEntity(out);
|
|
58
|
-
}
|
|
59
54
|
// Stops delivery to `app` and forgets its subscription. Safe to call when not attached.
|
|
60
55
|
export function detachAppLifecycle(app) {
|
|
61
56
|
const unsubscribe = _subscriptions.get(app);
|
|
@@ -64,37 +59,33 @@ export function detachAppLifecycle(app) {
|
|
|
64
59
|
_subscriptions.delete(app);
|
|
65
60
|
}
|
|
66
61
|
}
|
|
67
|
-
// Releases `app` for garbage collection by detaching its
|
|
62
|
+
// Releases `app` for garbage collection by detaching its provider subscription. The signals remain
|
|
68
63
|
// plain GC-managed memory afterward.
|
|
69
64
|
export function disposeAppLifecycle(app) {
|
|
70
65
|
detachAppLifecycle(app);
|
|
71
66
|
_savedState.delete(app);
|
|
72
67
|
}
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
// ★ The sentinel is deliberately not consulted. It answers every operation, so counting it would make
|
|
78
|
-
// this report `true` for everything and say nothing at all.
|
|
79
|
-
export function explainLifecycleOperation(host, operation) {
|
|
80
|
-
const implemented = typeof host.system.lifecycle[operation] === 'function';
|
|
68
|
+
// Reports whether the supplied provider implements `operation`. Optional methods that the provider
|
|
69
|
+
// omits are reported as the sentinel layer so callers can distinguish absence from a real operation.
|
|
70
|
+
export function explainLifecycleOperation(hostLifecycle, operation) {
|
|
71
|
+
const implemented = typeof hostLifecycle[operation] === 'function';
|
|
81
72
|
return { implemented, layer: implemented ? 'host' : 'sentinel', operation };
|
|
82
73
|
}
|
|
83
74
|
// Returns the kind of launch — 'cold' (fresh process) or 'warm' (resumed from background). The web
|
|
84
|
-
//
|
|
85
|
-
// others → 'cold'). Returns 'warm' as a safe fallback when the
|
|
86
|
-
// getLaunchKind (legacy or minimal
|
|
87
|
-
export function getAppLaunchKind(
|
|
88
|
-
const backend =
|
|
75
|
+
// provider approximates this via PerformanceNavigationTiming.type ('back_forward' → 'warm', all
|
|
76
|
+
// others → 'cold'). Returns 'warm' as a safe fallback when the provider does not implement
|
|
77
|
+
// getLaunchKind (legacy or minimal providers that pre-date the optional method).
|
|
78
|
+
export function getAppLaunchKind(hostLifecycle) {
|
|
79
|
+
const backend = hostLifecycle;
|
|
89
80
|
return backend.getLaunchKind !== undefined ? backend.getLaunchKind() : 'warm';
|
|
90
81
|
}
|
|
91
|
-
// Returns the current application lifecycle state from the
|
|
92
|
-
export function getAppLifecycleState(
|
|
93
|
-
return
|
|
82
|
+
// Returns the current application lifecycle state from the supplied provider.
|
|
83
|
+
export function getAppLifecycleState(hostLifecycle) {
|
|
84
|
+
return hostLifecycle.getState();
|
|
94
85
|
}
|
|
95
|
-
// Whether
|
|
96
|
-
export function hasLifecycleOperation(
|
|
97
|
-
return explainLifecycleOperation(
|
|
86
|
+
// Whether the supplied provider implements `operation`.
|
|
87
|
+
export function hasLifecycleOperation(hostLifecycle, operation) {
|
|
88
|
+
return explainLifecycleOperation(hostLifecycle, operation).implemented;
|
|
98
89
|
}
|
|
99
90
|
// Allocates an AppLifecycle event entity with inert signals; call attachAppLifecycle to start delivery.
|
|
100
91
|
export function initializeAppLifecycle(out) {
|
|
@@ -106,118 +97,18 @@ export function initializeAppLifecycle(out) {
|
|
|
106
97
|
out.onSaveState = createSignal();
|
|
107
98
|
out.onRestoreState = createSignal();
|
|
108
99
|
}
|
|
109
|
-
// Builds the default web backend over document visibility, window focus/blur, and pagehide/pageshow
|
|
110
|
-
// events. Produces three states:
|
|
111
|
-
// 'active' — document visible and window focused
|
|
112
|
-
// 'inactive' — document visible but window not focused (app switcher, control-center, other window)
|
|
113
|
-
// 'background' — document.hidden (tab hidden or page unloading)
|
|
114
|
-
// Degrades to state 'active' and a no-op subscription where document/window are absent (SSR/jsdom).
|
|
115
|
-
//
|
|
116
|
-
// getLaunchKind() approximates cold vs. warm using PerformanceNavigationTiming.type:
|
|
117
|
-
// 'back_forward' → 'warm' — page was restored from the bfcache (JS heap was frozen and thawed;
|
|
118
|
-
// the closest web equivalent of a process warm-resume)
|
|
119
|
-
// all others → 'cold' — 'navigate', 'reload', 'prerender' are each a fresh page lifecycle
|
|
120
|
-
// Falls back to 'cold' when the Performance Navigation Timing API is unavailable (SSR/jsdom).
|
|
121
|
-
//
|
|
122
|
-
// subscribeMemoryWarning() wires the experimental 'memory-pressure' window event (Chrome origin
|
|
123
|
-
// trial / behind flags). The event detail carries a 'critical' pressure string; this backend maps
|
|
124
|
-
// it to 'critical' and fires 'normal' on the subsequent resolution event when present. Falls back to
|
|
125
|
-
// no-op unsubscribe when the event is not supported (no standard API is widely deployed as of 2026).
|
|
126
|
-
export function initializeWebLifecycleBackend(out) {
|
|
127
|
-
let _windowFocused = typeof document !== 'undefined';
|
|
128
|
-
out.getState = () => {
|
|
129
|
-
if (typeof document === 'undefined')
|
|
130
|
-
return 'active';
|
|
131
|
-
if (document.hidden)
|
|
132
|
-
return 'background';
|
|
133
|
-
return _windowFocused ? 'active' : 'inactive';
|
|
134
|
-
};
|
|
135
|
-
out.subscribe = (listener) => {
|
|
136
|
-
if (typeof document === 'undefined' || typeof window === 'undefined')
|
|
137
|
-
return () => { };
|
|
138
|
-
// Initialize focus state at subscribe time.
|
|
139
|
-
_windowFocused = document.hasFocus();
|
|
140
|
-
const onFocus = () => {
|
|
141
|
-
_windowFocused = true;
|
|
142
|
-
listener();
|
|
143
|
-
};
|
|
144
|
-
const onBlur = () => {
|
|
145
|
-
_windowFocused = false;
|
|
146
|
-
listener();
|
|
147
|
-
};
|
|
148
|
-
document.addEventListener('visibilitychange', listener);
|
|
149
|
-
window.addEventListener('pagehide', listener);
|
|
150
|
-
window.addEventListener('pageshow', listener);
|
|
151
|
-
window.addEventListener('focus', onFocus);
|
|
152
|
-
window.addEventListener('blur', onBlur);
|
|
153
|
-
return () => {
|
|
154
|
-
document.removeEventListener('visibilitychange', listener);
|
|
155
|
-
window.removeEventListener('pagehide', listener);
|
|
156
|
-
window.removeEventListener('pageshow', listener);
|
|
157
|
-
window.removeEventListener('focus', onFocus);
|
|
158
|
-
window.removeEventListener('blur', onBlur);
|
|
159
|
-
};
|
|
160
|
-
};
|
|
161
|
-
out.getLaunchKind = () => {
|
|
162
|
-
// PerformanceNavigationTiming.type === 'back_forward' indicates the page was restored from the
|
|
163
|
-
// browser's back/forward cache (bfcache). The JS heap was frozen and thawed without a fresh
|
|
164
|
-
// process start — the closest analog to a mobile warm-resume. All other navigation types
|
|
165
|
-
// ('navigate', 'reload', 'prerender') are a fresh page lifecycle, i.e. cold.
|
|
166
|
-
if (typeof performance === 'undefined')
|
|
167
|
-
return 'cold';
|
|
168
|
-
const entries = performance.getEntriesByType('navigation');
|
|
169
|
-
if (entries.length > 0 && entries[0].type === 'back_forward')
|
|
170
|
-
return 'warm';
|
|
171
|
-
return 'cold';
|
|
172
|
-
};
|
|
173
|
-
out.subscribeMemoryWarning = (listener) => {
|
|
174
|
-
// The 'memory-pressure' window event is an experimental Chrome API (origin trial / behind
|
|
175
|
-
// flags as of 2026). The event carries a detail object with a 'pressure' string. This backend
|
|
176
|
-
// maps 'critical' pressure to AppMemoryPressure 'critical'; a pressure-resolved / 'moderate'
|
|
177
|
-
// event maps to 'moderate'. Falls back to no-op unsubscribe when the event is not supported
|
|
178
|
-
// (no cross-browser API for memory pressure is widely deployed yet).
|
|
179
|
-
if (typeof window === 'undefined')
|
|
180
|
-
return () => { };
|
|
181
|
-
// Feature-detect: attempt to register and immediately remove a passive listener to check
|
|
182
|
-
// whether the event is supported. If addEventListener silently ignores the event type there
|
|
183
|
-
// is nothing we can do — we just return no-op without pretending to be subscribed.
|
|
184
|
-
const onPressure = (e) => {
|
|
185
|
-
const detail = e.detail;
|
|
186
|
-
const pressure = detail?.pressure;
|
|
187
|
-
if (pressure === 'critical') {
|
|
188
|
-
listener('critical');
|
|
189
|
-
}
|
|
190
|
-
else if (pressure === 'moderate') {
|
|
191
|
-
listener('moderate');
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
// Unknown pressure level — treat as moderate rather than silently dropping.
|
|
195
|
-
listener('moderate');
|
|
196
|
-
}
|
|
197
|
-
};
|
|
198
|
-
const onPressureRelieved = () => {
|
|
199
|
-
listener('normal');
|
|
200
|
-
};
|
|
201
|
-
window.addEventListener('memory-pressure', onPressure);
|
|
202
|
-
window.addEventListener('memory-pressure-relieved', onPressureRelieved);
|
|
203
|
-
return () => {
|
|
204
|
-
window.removeEventListener('memory-pressure', onPressure);
|
|
205
|
-
window.removeEventListener('memory-pressure-relieved', onPressureRelieved);
|
|
206
|
-
};
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
100
|
// Returns true when the application is in the 'active' state (visible and focused).
|
|
210
|
-
export function isAppActive(
|
|
211
|
-
return
|
|
101
|
+
export function isAppActive(hostLifecycle) {
|
|
102
|
+
return hostLifecycle.getState() === 'active';
|
|
212
103
|
}
|
|
213
104
|
// Returns true when the application is in the 'background' state (hidden/suspended).
|
|
214
|
-
export function isAppBackground(
|
|
215
|
-
return
|
|
105
|
+
export function isAppBackground(hostLifecycle) {
|
|
106
|
+
return hostLifecycle.getState() === 'background';
|
|
216
107
|
}
|
|
217
108
|
// Returns true when the application is in the 'inactive' state (visible but not focused —
|
|
218
109
|
// e.g. app switcher, control-center overlay, incoming call).
|
|
219
|
-
export function isAppInactive(
|
|
220
|
-
return
|
|
110
|
+
export function isAppInactive(hostLifecycle) {
|
|
111
|
+
return hostLifecycle.getState() === 'inactive';
|
|
221
112
|
}
|
|
222
113
|
// Emits app.onBackButton and returns whether the back action may proceed. Returns false when a
|
|
223
114
|
// listener vetoed by calling cancelSignal(app.onBackButton), meaning the listener handled
|
package/dist/lifecycle.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../src/lifecycle.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzE,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;
|
|
1
|
+
{"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../src/lifecycle.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flighthq/lifecycle",
|
|
3
|
-
"version": "0.5.1-edge.
|
|
3
|
+
"version": "0.5.1-edge.1350.8ce488b",
|
|
4
4
|
"author": "Joshua Granick and other contributors",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,9 +38,9 @@
|
|
|
38
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@flighthq/entity": "0.5.1-edge.
|
|
42
|
-
"@flighthq/signals": "0.5.1-edge.
|
|
43
|
-
"@flighthq/types": "0.5.1-edge.
|
|
41
|
+
"@flighthq/entity": "0.5.1-edge.1350.8ce488b",
|
|
42
|
+
"@flighthq/signals": "0.5.1-edge.1350.8ce488b",
|
|
43
|
+
"@flighthq/types": "0.5.1-edge.1350.8ce488b"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"typescript": "^5.3.0"
|
package/src/lifecycle.test.ts
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
import { allocateEntity, finishEntity } from '@flighthq/entity/contract';
|
|
2
2
|
import { cancelSignal, connectSignal } from '@flighthq/signals/contract';
|
|
3
3
|
import type {
|
|
4
|
-
|
|
4
|
+
HostLifecycleProvider,
|
|
5
5
|
AppLaunchKind,
|
|
6
6
|
AppLifecycleState,
|
|
7
7
|
AppMemoryPressure,
|
|
8
8
|
Entity,
|
|
9
|
-
LifecycleBackend,
|
|
10
9
|
LifecycleOperation,
|
|
11
10
|
} from '@flighthq/types/contract';
|
|
12
11
|
|
|
13
12
|
import {
|
|
14
13
|
attachAppLifecycle,
|
|
15
14
|
createAppLifecycle,
|
|
16
|
-
createWebLifecycleBackend,
|
|
17
15
|
detachAppLifecycle,
|
|
18
16
|
disposeAppLifecycle,
|
|
19
17
|
explainLifecycleOperation,
|
|
@@ -21,21 +19,20 @@ import {
|
|
|
21
19
|
getAppLifecycleState,
|
|
22
20
|
hasLifecycleOperation,
|
|
23
21
|
initializeAppLifecycle,
|
|
24
|
-
initializeWebLifecycleBackend,
|
|
25
22
|
isAppActive,
|
|
26
23
|
isAppBackground,
|
|
27
24
|
isAppInactive,
|
|
28
25
|
requestAppBack,
|
|
29
26
|
} from './lifecycle';
|
|
30
27
|
|
|
31
|
-
type FakeBackend =
|
|
28
|
+
type FakeBackend = HostLifecycleProvider & {
|
|
32
29
|
state: AppLifecycleState;
|
|
33
30
|
fire: () => void;
|
|
34
31
|
fireMemory: (level: AppMemoryPressure) => void;
|
|
35
32
|
};
|
|
36
33
|
|
|
37
|
-
function hostOf(backend:
|
|
38
|
-
return { system: { lifecycle: backend } } as
|
|
34
|
+
function hostOf(backend: HostLifecycleProvider): { readonly system: { readonly lifecycle: HostLifecycleProvider } } {
|
|
35
|
+
return { system: { lifecycle: backend } } as { readonly system: { readonly lifecycle: HostLifecycleProvider } };
|
|
39
36
|
}
|
|
40
37
|
|
|
41
38
|
function fakeBackend(): FakeBackend {
|
|
@@ -76,7 +73,7 @@ describe('attachAppLifecycle', () => {
|
|
|
76
73
|
let pauses = 0;
|
|
77
74
|
connectSignal(app.onStateChange, () => changes++);
|
|
78
75
|
connectSignal(app.onPause, () => pauses++);
|
|
79
|
-
attachAppLifecycle(host, app);
|
|
76
|
+
attachAppLifecycle(host.system.lifecycle, app);
|
|
80
77
|
backend.state = 'background';
|
|
81
78
|
backend.fire();
|
|
82
79
|
expect(changes).toBe(1);
|
|
@@ -89,7 +86,7 @@ describe('attachAppLifecycle', () => {
|
|
|
89
86
|
const app = createAppLifecycle();
|
|
90
87
|
let pauses = 0;
|
|
91
88
|
connectSignal(app.onPause, () => pauses++);
|
|
92
|
-
attachAppLifecycle(host, app);
|
|
89
|
+
attachAppLifecycle(host.system.lifecycle, app);
|
|
93
90
|
backend.state = 'inactive';
|
|
94
91
|
backend.fire();
|
|
95
92
|
expect(pauses).toBe(1);
|
|
@@ -102,7 +99,7 @@ describe('attachAppLifecycle', () => {
|
|
|
102
99
|
const app = createAppLifecycle();
|
|
103
100
|
let pauses = 0;
|
|
104
101
|
connectSignal(app.onPause, () => pauses++);
|
|
105
|
-
attachAppLifecycle(host, app);
|
|
102
|
+
attachAppLifecycle(host.system.lifecycle, app);
|
|
106
103
|
backend.state = 'background';
|
|
107
104
|
backend.fire();
|
|
108
105
|
// inactive → background should not re-fire onPause (already paused)
|
|
@@ -116,7 +113,7 @@ describe('attachAppLifecycle', () => {
|
|
|
116
113
|
const app = createAppLifecycle();
|
|
117
114
|
let resumes = 0;
|
|
118
115
|
connectSignal(app.onResume, () => resumes++);
|
|
119
|
-
attachAppLifecycle(host, app);
|
|
116
|
+
attachAppLifecycle(host.system.lifecycle, app);
|
|
120
117
|
backend.state = 'active';
|
|
121
118
|
backend.fire();
|
|
122
119
|
expect(resumes).toBe(1);
|
|
@@ -129,7 +126,7 @@ describe('attachAppLifecycle', () => {
|
|
|
129
126
|
const app = createAppLifecycle();
|
|
130
127
|
let resumes = 0;
|
|
131
128
|
connectSignal(app.onResume, () => resumes++);
|
|
132
|
-
attachAppLifecycle(host, app);
|
|
129
|
+
attachAppLifecycle(host.system.lifecycle, app);
|
|
133
130
|
backend.state = 'active';
|
|
134
131
|
backend.fire();
|
|
135
132
|
expect(resumes).toBe(1);
|
|
@@ -141,8 +138,8 @@ describe('attachAppLifecycle', () => {
|
|
|
141
138
|
const app = createAppLifecycle();
|
|
142
139
|
let changes = 0;
|
|
143
140
|
connectSignal(app.onStateChange, () => changes++);
|
|
144
|
-
attachAppLifecycle(host, app);
|
|
145
|
-
attachAppLifecycle(host, app);
|
|
141
|
+
attachAppLifecycle(host.system.lifecycle, app);
|
|
142
|
+
attachAppLifecycle(host.system.lifecycle, app);
|
|
146
143
|
backend.state = 'background';
|
|
147
144
|
backend.fire();
|
|
148
145
|
// Only one subscription should be active.
|
|
@@ -158,7 +155,7 @@ describe('attachAppLifecycle', () => {
|
|
|
158
155
|
bag['key'] = 'value';
|
|
159
156
|
saved = true;
|
|
160
157
|
});
|
|
161
|
-
attachAppLifecycle(host, app);
|
|
158
|
+
attachAppLifecycle(host.system.lifecycle, app);
|
|
162
159
|
backend.state = 'background';
|
|
163
160
|
backend.fire();
|
|
164
161
|
expect(saved).toBe(true);
|
|
@@ -175,7 +172,7 @@ describe('attachAppLifecycle', () => {
|
|
|
175
172
|
connectSignal(app.onRestoreState, (state) => {
|
|
176
173
|
restored = state;
|
|
177
174
|
});
|
|
178
|
-
attachAppLifecycle(host, app);
|
|
175
|
+
attachAppLifecycle(host.system.lifecycle, app);
|
|
179
176
|
// Transition away to trigger save.
|
|
180
177
|
backend.state = 'background';
|
|
181
178
|
backend.fire();
|
|
@@ -192,7 +189,7 @@ describe('attachAppLifecycle', () => {
|
|
|
192
189
|
const app = createAppLifecycle();
|
|
193
190
|
const levels: AppMemoryPressure[] = [];
|
|
194
191
|
connectSignal(app.onMemoryWarning, (level) => levels.push(level));
|
|
195
|
-
attachAppLifecycle(host, app);
|
|
192
|
+
attachAppLifecycle(host.system.lifecycle, app);
|
|
196
193
|
backend.fireMemory('critical');
|
|
197
194
|
expect(levels).toEqual(['critical']);
|
|
198
195
|
});
|
|
@@ -210,7 +207,7 @@ describe('attachAppLifecycle', () => {
|
|
|
210
207
|
const app = createAppLifecycle();
|
|
211
208
|
let changes = 0;
|
|
212
209
|
connectSignal(app.onStateChange, () => changes++);
|
|
213
|
-
attachAppLifecycle(host, app);
|
|
210
|
+
attachAppLifecycle(host.system.lifecycle, app);
|
|
214
211
|
const fireCount = 1 + Math.floor(Math.random() * 30);
|
|
215
212
|
for (let i = 0; i < fireCount; i++) {
|
|
216
213
|
backend.state = states[Math.floor(Math.random() * states.length)];
|
|
@@ -234,7 +231,7 @@ describe('attachAppLifecycle', () => {
|
|
|
234
231
|
let resumes = 0;
|
|
235
232
|
connectSignal(app.onPause, () => pauses++);
|
|
236
233
|
connectSignal(app.onResume, () => resumes++);
|
|
237
|
-
attachAppLifecycle(host, app);
|
|
234
|
+
attachAppLifecycle(host.system.lifecycle, app);
|
|
238
235
|
|
|
239
236
|
// Drive a random storm while computing the expected edge counts from an independent model:
|
|
240
237
|
// a pause edge is every 'active' → non-'active' transition, a resume edge every
|
|
@@ -272,7 +269,7 @@ describe('attachAppLifecycle', () => {
|
|
|
272
269
|
connectSignal(app.onStateChange, () => changes++);
|
|
273
270
|
connectSignal(app.onPause, () => pauses++);
|
|
274
271
|
connectSignal(app.onResume, () => resumes++);
|
|
275
|
-
attachAppLifecycle(host, app);
|
|
272
|
+
attachAppLifecycle(host.system.lifecycle, app);
|
|
276
273
|
const fireCount = 1 + Math.floor(Math.random() * 30);
|
|
277
274
|
for (let i = 0; i < fireCount; i++) backend.fire();
|
|
278
275
|
expect(changes).toBe(fireCount);
|
|
@@ -297,7 +294,7 @@ describe('attachAppLifecycle', () => {
|
|
|
297
294
|
let resumes = 0;
|
|
298
295
|
connectSignal(app.onPause, () => pauses++);
|
|
299
296
|
connectSignal(app.onResume, () => resumes++);
|
|
300
|
-
attachAppLifecycle(host, app);
|
|
297
|
+
attachAppLifecycle(host.system.lifecycle, app);
|
|
301
298
|
const fireCount = 1 + Math.floor(Math.random() * 30);
|
|
302
299
|
for (let i = 0; i < fireCount; i++) {
|
|
303
300
|
backend.state = states[Math.floor(Math.random() * states.length)];
|
|
@@ -322,88 +319,6 @@ describe('createAppLifecycle', () => {
|
|
|
322
319
|
});
|
|
323
320
|
});
|
|
324
321
|
|
|
325
|
-
describe('createWebLifecycleBackend', () => {
|
|
326
|
-
it("getLaunchKind returns 'cold' when no performance navigation entries", () => {
|
|
327
|
-
vi.spyOn(performance, 'getEntriesByType').mockReturnValue([]);
|
|
328
|
-
const backend = createWebLifecycleBackend();
|
|
329
|
-
expect(backend.getLaunchKind?.()).toBe('cold');
|
|
330
|
-
vi.restoreAllMocks();
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
it("getLaunchKind returns 'warm' for back_forward navigation type", () => {
|
|
334
|
-
vi.spyOn(performance, 'getEntriesByType').mockReturnValue([
|
|
335
|
-
{ type: 'back_forward' } as PerformanceNavigationTiming,
|
|
336
|
-
]);
|
|
337
|
-
const backend = createWebLifecycleBackend();
|
|
338
|
-
expect(backend.getLaunchKind?.()).toBe('warm');
|
|
339
|
-
vi.restoreAllMocks();
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it("getLaunchKind returns 'cold' for reload navigation type", () => {
|
|
343
|
-
vi.spyOn(performance, 'getEntriesByType').mockReturnValue([{ type: 'reload' } as PerformanceNavigationTiming]);
|
|
344
|
-
const backend = createWebLifecycleBackend();
|
|
345
|
-
expect(backend.getLaunchKind?.()).toBe('cold');
|
|
346
|
-
vi.restoreAllMocks();
|
|
347
|
-
});
|
|
348
|
-
|
|
349
|
-
it("getLaunchKind returns 'cold' for navigate navigation type", () => {
|
|
350
|
-
vi.spyOn(performance, 'getEntriesByType').mockReturnValue([{ type: 'navigate' } as PerformanceNavigationTiming]);
|
|
351
|
-
const backend = createWebLifecycleBackend();
|
|
352
|
-
expect(backend.getLaunchKind?.()).toBe('cold');
|
|
353
|
-
vi.restoreAllMocks();
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
it('reads a state without throwing', () => {
|
|
357
|
-
expect(typeof createWebLifecycleBackend().getState()).toBe('string');
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
it('subscribeMemoryWarning fires critical when memory-pressure event fires', () => {
|
|
361
|
-
const backend = createWebLifecycleBackend();
|
|
362
|
-
const levels: AppMemoryPressure[] = [];
|
|
363
|
-
const unsubscribe = backend.subscribeMemoryWarning?.((level) => levels.push(level));
|
|
364
|
-
// Simulate a memory-pressure event with 'critical' pressure detail.
|
|
365
|
-
const event = new CustomEvent('memory-pressure', { detail: { pressure: 'critical' } });
|
|
366
|
-
window.dispatchEvent(event);
|
|
367
|
-
expect(levels).toEqual(['critical']);
|
|
368
|
-
unsubscribe?.();
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it('subscribeMemoryWarning fires moderate when memory-pressure event fires with moderate pressure', () => {
|
|
372
|
-
const backend = createWebLifecycleBackend();
|
|
373
|
-
const levels: AppMemoryPressure[] = [];
|
|
374
|
-
const unsubscribe = backend.subscribeMemoryWarning?.((level) => levels.push(level));
|
|
375
|
-
const event = new CustomEvent('memory-pressure', { detail: { pressure: 'moderate' } });
|
|
376
|
-
window.dispatchEvent(event);
|
|
377
|
-
expect(levels).toEqual(['moderate']);
|
|
378
|
-
unsubscribe?.();
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
it('subscribeMemoryWarning fires normal when memory-pressure-relieved event fires', () => {
|
|
382
|
-
const backend = createWebLifecycleBackend();
|
|
383
|
-
const levels: AppMemoryPressure[] = [];
|
|
384
|
-
const unsubscribe = backend.subscribeMemoryWarning?.((level) => levels.push(level));
|
|
385
|
-
const event = new CustomEvent('memory-pressure-relieved');
|
|
386
|
-
window.dispatchEvent(event);
|
|
387
|
-
expect(levels).toEqual(['normal']);
|
|
388
|
-
unsubscribe?.();
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
it('subscribeMemoryWarning stops delivering after unsubscribe', () => {
|
|
392
|
-
const backend = createWebLifecycleBackend();
|
|
393
|
-
const levels: AppMemoryPressure[] = [];
|
|
394
|
-
const unsubscribe = backend.subscribeMemoryWarning?.((level) => levels.push(level));
|
|
395
|
-
unsubscribe?.();
|
|
396
|
-
const event = new CustomEvent('memory-pressure', { detail: { pressure: 'critical' } });
|
|
397
|
-
window.dispatchEvent(event);
|
|
398
|
-
expect(levels).toHaveLength(0);
|
|
399
|
-
});
|
|
400
|
-
|
|
401
|
-
it('subscribes and unsubscribes without throwing', () => {
|
|
402
|
-
const unsubscribe = createWebLifecycleBackend().subscribe(() => {});
|
|
403
|
-
expect(() => unsubscribe()).not.toThrow();
|
|
404
|
-
});
|
|
405
|
-
});
|
|
406
|
-
|
|
407
322
|
describe('detachAppLifecycle', () => {
|
|
408
323
|
it('stops further delivery', () => {
|
|
409
324
|
const backend = fakeBackend();
|
|
@@ -411,7 +326,7 @@ describe('detachAppLifecycle', () => {
|
|
|
411
326
|
const app = createAppLifecycle();
|
|
412
327
|
let changes = 0;
|
|
413
328
|
connectSignal(app.onStateChange, () => changes++);
|
|
414
|
-
attachAppLifecycle(host, app);
|
|
329
|
+
attachAppLifecycle(host.system.lifecycle, app);
|
|
415
330
|
detachAppLifecycle(app);
|
|
416
331
|
backend.fire();
|
|
417
332
|
expect(changes).toBe(0);
|
|
@@ -428,7 +343,7 @@ describe('disposeAppLifecycle', () => {
|
|
|
428
343
|
const backend = fakeBackend();
|
|
429
344
|
const host = hostOf(backend);
|
|
430
345
|
const app = createAppLifecycle();
|
|
431
|
-
attachAppLifecycle(host, app);
|
|
346
|
+
attachAppLifecycle(host.system.lifecycle, app);
|
|
432
347
|
expect(() => disposeAppLifecycle(app)).not.toThrow();
|
|
433
348
|
});
|
|
434
349
|
|
|
@@ -438,7 +353,7 @@ describe('disposeAppLifecycle', () => {
|
|
|
438
353
|
const app = createAppLifecycle();
|
|
439
354
|
let changes = 0;
|
|
440
355
|
connectSignal(app.onStateChange, () => changes++);
|
|
441
|
-
attachAppLifecycle(host, app);
|
|
356
|
+
attachAppLifecycle(host.system.lifecycle, app);
|
|
442
357
|
disposeAppLifecycle(app);
|
|
443
358
|
backend.fire();
|
|
444
359
|
expect(changes).toBe(0);
|
|
@@ -451,7 +366,7 @@ describe('explainLifecycleOperation', () => {
|
|
|
451
366
|
// about a process-wide fallback that answered everything.
|
|
452
367
|
it('reports the host provider as implementing what it provides', () => {
|
|
453
368
|
const host = hostOf(fakeBackend());
|
|
454
|
-
expect(explainLifecycleOperation(host, 'getState')).toEqual({
|
|
369
|
+
expect(explainLifecycleOperation(host.system.lifecycle, 'getState')).toEqual({
|
|
455
370
|
implemented: true,
|
|
456
371
|
layer: 'host',
|
|
457
372
|
operation: 'getState',
|
|
@@ -461,13 +376,13 @@ describe('explainLifecycleOperation', () => {
|
|
|
461
376
|
it('reports an operation the host provider omits as unimplemented', () => {
|
|
462
377
|
const host = hostOf(
|
|
463
378
|
(() => {
|
|
464
|
-
const out = allocateEntity<
|
|
379
|
+
const out = allocateEntity<HostLifecycleProvider>();
|
|
465
380
|
out.getState = () => 'active' as const;
|
|
466
381
|
out.subscribe = () => () => {};
|
|
467
382
|
return finishEntity(out);
|
|
468
383
|
})(),
|
|
469
384
|
);
|
|
470
|
-
expect(explainLifecycleOperation(host, 'getLaunchKind')).toEqual({
|
|
385
|
+
expect(explainLifecycleOperation(host.system.lifecycle, 'getLaunchKind')).toEqual({
|
|
471
386
|
implemented: false,
|
|
472
387
|
layer: 'sentinel',
|
|
473
388
|
operation: 'getLaunchKind',
|
|
@@ -480,14 +395,14 @@ describe('explainLifecycleOperation', () => {
|
|
|
480
395
|
const rich = hostOf(fakeBackend());
|
|
481
396
|
const bare = hostOf(
|
|
482
397
|
(() => {
|
|
483
|
-
const out = allocateEntity<
|
|
398
|
+
const out = allocateEntity<HostLifecycleProvider>();
|
|
484
399
|
out.getState = () => 'active' as const;
|
|
485
400
|
out.subscribe = () => () => {};
|
|
486
401
|
return finishEntity(out);
|
|
487
402
|
})(),
|
|
488
403
|
);
|
|
489
|
-
expect(explainLifecycleOperation(rich, 'subscribeMemoryWarning').implemented).toBe(true);
|
|
490
|
-
expect(explainLifecycleOperation(bare, 'subscribeMemoryWarning').implemented).toBe(false);
|
|
404
|
+
expect(explainLifecycleOperation(rich.system.lifecycle, 'subscribeMemoryWarning').implemented).toBe(true);
|
|
405
|
+
expect(explainLifecycleOperation(bare.system.lifecycle, 'subscribeMemoryWarning').implemented).toBe(false);
|
|
491
406
|
});
|
|
492
407
|
});
|
|
493
408
|
|
|
@@ -495,17 +410,17 @@ describe('getAppLaunchKind', () => {
|
|
|
495
410
|
it("returns 'warm' when backend does not implement getLaunchKind", () => {
|
|
496
411
|
const backend = fakeBackend();
|
|
497
412
|
const host = hostOf(backend);
|
|
498
|
-
expect(getAppLaunchKind(host)).toBe('warm');
|
|
413
|
+
expect(getAppLaunchKind(host.system.lifecycle)).toBe('warm');
|
|
499
414
|
});
|
|
500
415
|
|
|
501
416
|
it('delegates to backend.getLaunchKind when present', () => {
|
|
502
|
-
const out = allocateEntity<
|
|
417
|
+
const out = allocateEntity<HostLifecycleProvider>();
|
|
503
418
|
out.getState = () => 'active' as AppLifecycleState;
|
|
504
419
|
out.subscribe = () => () => {};
|
|
505
420
|
out.getLaunchKind = () => 'cold' as AppLaunchKind;
|
|
506
421
|
const backend = finishEntity(out);
|
|
507
422
|
const host = hostOf(backend);
|
|
508
|
-
expect(getAppLaunchKind(host)).toBe('cold');
|
|
423
|
+
expect(getAppLaunchKind(host.system.lifecycle)).toBe('cold');
|
|
509
424
|
});
|
|
510
425
|
});
|
|
511
426
|
|
|
@@ -514,26 +429,28 @@ describe('getAppLifecycleState', () => {
|
|
|
514
429
|
const backend = fakeBackend();
|
|
515
430
|
backend.state = 'inactive';
|
|
516
431
|
const host = hostOf(backend);
|
|
517
|
-
expect(getAppLifecycleState(host)).toBe('inactive');
|
|
432
|
+
expect(getAppLifecycleState(host.system.lifecycle)).toBe('inactive');
|
|
518
433
|
});
|
|
519
434
|
});
|
|
520
435
|
|
|
521
436
|
describe('hasLifecycleOperation', () => {
|
|
522
437
|
it('agrees with explainLifecycleOperation for the same host', () => {
|
|
523
438
|
const host = hostOf(fakeBackend());
|
|
524
|
-
expect(hasLifecycleOperation(host, 'getState')).toBe(
|
|
439
|
+
expect(hasLifecycleOperation(host.system.lifecycle, 'getState')).toBe(
|
|
440
|
+
explainLifecycleOperation(host.system.lifecycle, 'getState').implemented,
|
|
441
|
+
);
|
|
525
442
|
});
|
|
526
443
|
|
|
527
444
|
it('is false for an operation the host provider omits', () => {
|
|
528
445
|
const host = hostOf(
|
|
529
446
|
(() => {
|
|
530
|
-
const out = allocateEntity<
|
|
447
|
+
const out = allocateEntity<HostLifecycleProvider>();
|
|
531
448
|
out.getState = () => 'active' as const;
|
|
532
449
|
out.subscribe = () => () => {};
|
|
533
450
|
return finishEntity(out);
|
|
534
451
|
})(),
|
|
535
452
|
);
|
|
536
|
-
expect(hasLifecycleOperation(host, 'getLaunchKind')).toBe(false);
|
|
453
|
+
expect(hasLifecycleOperation(host.system.lifecycle, 'getLaunchKind')).toBe(false);
|
|
537
454
|
});
|
|
538
455
|
});
|
|
539
456
|
|
|
@@ -543,20 +460,14 @@ describe('initializeAppLifecycle', () => {
|
|
|
543
460
|
});
|
|
544
461
|
});
|
|
545
462
|
|
|
546
|
-
describe('initializeWebLifecycleBackend', () => {
|
|
547
|
-
it('is the construction initializer of createWebLifecycleBackend', () => {
|
|
548
|
-
expect(typeof initializeWebLifecycleBackend).toBe('function');
|
|
549
|
-
});
|
|
550
|
-
});
|
|
551
|
-
|
|
552
463
|
// Per-operation availability for LifecycleBackend. The operations below are the ones the interface declares
|
|
553
464
|
// OPTIONAL, so a host that omits them is compliant rather than broken — that is the absence-of-an-export
|
|
554
465
|
// ruling, and this is the query that makes it observable.
|
|
555
466
|
const OPTIONAL_OPERATIONS: readonly LifecycleOperation[] = ['getLaunchKind', 'subscribeMemoryWarning'];
|
|
556
467
|
|
|
557
468
|
// A host implementing only the REQUIRED members — partial support declared by absence.
|
|
558
|
-
function partialBackend():
|
|
559
|
-
const out = allocateEntity<
|
|
469
|
+
function partialBackend(): HostLifecycleProvider {
|
|
470
|
+
const out = allocateEntity<HostLifecycleProvider>();
|
|
560
471
|
out.getState = (() => undefined) as never;
|
|
561
472
|
out.subscribe = (() => undefined) as never;
|
|
562
473
|
return finishEntity(out);
|
|
@@ -567,21 +478,21 @@ describe('isAppActive', () => {
|
|
|
567
478
|
const backend = fakeBackend();
|
|
568
479
|
backend.state = 'active';
|
|
569
480
|
const host = hostOf(backend);
|
|
570
|
-
expect(isAppActive(host)).toBe(true);
|
|
481
|
+
expect(isAppActive(host.system.lifecycle)).toBe(true);
|
|
571
482
|
});
|
|
572
483
|
|
|
573
484
|
it("returns false when state is 'background'", () => {
|
|
574
485
|
const backend = fakeBackend();
|
|
575
486
|
backend.state = 'background';
|
|
576
487
|
const host = hostOf(backend);
|
|
577
|
-
expect(isAppActive(host)).toBe(false);
|
|
488
|
+
expect(isAppActive(host.system.lifecycle)).toBe(false);
|
|
578
489
|
});
|
|
579
490
|
|
|
580
491
|
it("returns false when state is 'inactive'", () => {
|
|
581
492
|
const backend = fakeBackend();
|
|
582
493
|
backend.state = 'inactive';
|
|
583
494
|
const host = hostOf(backend);
|
|
584
|
-
expect(isAppActive(host)).toBe(false);
|
|
495
|
+
expect(isAppActive(host.system.lifecycle)).toBe(false);
|
|
585
496
|
});
|
|
586
497
|
});
|
|
587
498
|
|
|
@@ -590,14 +501,14 @@ describe('isAppBackground', () => {
|
|
|
590
501
|
const backend = fakeBackend();
|
|
591
502
|
backend.state = 'background';
|
|
592
503
|
const host = hostOf(backend);
|
|
593
|
-
expect(isAppBackground(host)).toBe(true);
|
|
504
|
+
expect(isAppBackground(host.system.lifecycle)).toBe(true);
|
|
594
505
|
});
|
|
595
506
|
|
|
596
507
|
it("returns false when state is 'active'", () => {
|
|
597
508
|
const backend = fakeBackend();
|
|
598
509
|
backend.state = 'active';
|
|
599
510
|
const host = hostOf(backend);
|
|
600
|
-
expect(isAppBackground(host)).toBe(false);
|
|
511
|
+
expect(isAppBackground(host.system.lifecycle)).toBe(false);
|
|
601
512
|
});
|
|
602
513
|
});
|
|
603
514
|
describe('isAppInactive', () => {
|
|
@@ -605,14 +516,14 @@ describe('isAppInactive', () => {
|
|
|
605
516
|
const backend = fakeBackend();
|
|
606
517
|
backend.state = 'inactive';
|
|
607
518
|
const host = hostOf(backend);
|
|
608
|
-
expect(isAppInactive(host)).toBe(true);
|
|
519
|
+
expect(isAppInactive(host.system.lifecycle)).toBe(true);
|
|
609
520
|
});
|
|
610
521
|
|
|
611
522
|
it("returns false when state is 'active'", () => {
|
|
612
523
|
const backend = fakeBackend();
|
|
613
524
|
backend.state = 'active';
|
|
614
525
|
const host = hostOf(backend);
|
|
615
|
-
expect(isAppInactive(host)).toBe(false);
|
|
526
|
+
expect(isAppInactive(host.system.lifecycle)).toBe(false);
|
|
616
527
|
});
|
|
617
528
|
});
|
|
618
529
|
|
|
@@ -1,57 +1,63 @@
|
|
|
1
|
-
import type { AppLifecycleState,
|
|
1
|
+
import type { AppLifecycleState, HostLifecycleProvider } from '@flighthq/types/contract';
|
|
2
2
|
|
|
3
3
|
import { getAppLaunchKind, getAppLifecycleState, isAppActive, isAppBackground, isAppInactive } from './lifecycle';
|
|
4
4
|
|
|
5
|
-
function hostWith(backend: Partial<
|
|
6
|
-
|
|
5
|
+
function hostWith(backend: Partial<HostLifecycleProvider>): {
|
|
6
|
+
readonly system: { readonly lifecycle: HostLifecycleProvider };
|
|
7
|
+
} {
|
|
8
|
+
return { system: { lifecycle: backend as HostLifecycleProvider } } as {
|
|
9
|
+
readonly system: { readonly lifecycle: HostLifecycleProvider };
|
|
10
|
+
};
|
|
7
11
|
}
|
|
8
12
|
|
|
9
|
-
function stateHost(state: AppLifecycleState):
|
|
13
|
+
function stateHost(state: AppLifecycleState): { readonly system: { readonly lifecycle: HostLifecycleProvider } } {
|
|
10
14
|
return hostWith({ getState: () => state, subscribe: () => () => {} });
|
|
11
15
|
}
|
|
12
16
|
|
|
13
17
|
describe('getAppLaunchKind', () => {
|
|
14
18
|
it('reads the launch kind from the host it is given', () => {
|
|
15
|
-
expect(getAppLaunchKind(hostWith({ getState: () => 'active', getLaunchKind: () => 'warm' }))).toBe(
|
|
19
|
+
expect(getAppLaunchKind(hostWith({ getState: () => 'active', getLaunchKind: () => 'warm' }).system.lifecycle)).toBe(
|
|
20
|
+
'warm',
|
|
21
|
+
);
|
|
16
22
|
});
|
|
17
23
|
|
|
18
24
|
// getLaunchKind is optional on the backend; a provider without it must not throw.
|
|
19
25
|
it('falls back to warm when the provider does not implement it', () => {
|
|
20
|
-
expect(getAppLaunchKind(stateHost('active'))).toBe('warm');
|
|
26
|
+
expect(getAppLaunchKind(stateHost('active').system.lifecycle)).toBe('warm');
|
|
21
27
|
});
|
|
22
28
|
});
|
|
23
29
|
|
|
24
30
|
describe('getAppLifecycleState', () => {
|
|
25
31
|
it('reads the state from the host it is given', () => {
|
|
26
|
-
expect(getAppLifecycleState(stateHost('background'))).toBe('background');
|
|
32
|
+
expect(getAppLifecycleState(stateHost('background').system.lifecycle)).toBe('background');
|
|
27
33
|
});
|
|
28
34
|
|
|
29
35
|
// Two hosts, two answers, no ambient selection in between. This is the property the migration is
|
|
30
36
|
// for: before it, both calls resolved one process-wide backend and the second host was unreachable.
|
|
31
37
|
it('keeps two hosts independent', () => {
|
|
32
|
-
expect(getAppLifecycleState(stateHost('active'))).toBe('active');
|
|
33
|
-
expect(getAppLifecycleState(stateHost('inactive'))).toBe('inactive');
|
|
38
|
+
expect(getAppLifecycleState(stateHost('active').system.lifecycle)).toBe('active');
|
|
39
|
+
expect(getAppLifecycleState(stateHost('inactive').system.lifecycle)).toBe('inactive');
|
|
34
40
|
});
|
|
35
41
|
});
|
|
36
42
|
|
|
37
43
|
describe('isAppActive', () => {
|
|
38
44
|
it('reports active only for the active state', () => {
|
|
39
|
-
expect(isAppActive(stateHost('active'))).toBe(true);
|
|
40
|
-
expect(isAppActive(stateHost('inactive'))).toBe(false);
|
|
41
|
-
expect(isAppActive(stateHost('background'))).toBe(false);
|
|
45
|
+
expect(isAppActive(stateHost('active').system.lifecycle)).toBe(true);
|
|
46
|
+
expect(isAppActive(stateHost('inactive').system.lifecycle)).toBe(false);
|
|
47
|
+
expect(isAppActive(stateHost('background').system.lifecycle)).toBe(false);
|
|
42
48
|
});
|
|
43
49
|
});
|
|
44
50
|
|
|
45
51
|
describe('isAppBackground', () => {
|
|
46
52
|
it('reports background only for the background state', () => {
|
|
47
|
-
expect(isAppBackground(stateHost('background'))).toBe(true);
|
|
48
|
-
expect(isAppBackground(stateHost('active'))).toBe(false);
|
|
53
|
+
expect(isAppBackground(stateHost('background').system.lifecycle)).toBe(true);
|
|
54
|
+
expect(isAppBackground(stateHost('active').system.lifecycle)).toBe(false);
|
|
49
55
|
});
|
|
50
56
|
});
|
|
51
57
|
|
|
52
58
|
describe('isAppInactive', () => {
|
|
53
59
|
it('reports inactive only for the inactive state', () => {
|
|
54
|
-
expect(isAppInactive(stateHost('inactive'))).toBe(true);
|
|
55
|
-
expect(isAppInactive(stateHost('active'))).toBe(false);
|
|
60
|
+
expect(isAppInactive(stateHost('inactive').system.lifecycle)).toBe(true);
|
|
61
|
+
expect(isAppInactive(stateHost('active').system.lifecycle)).toBe(false);
|
|
56
62
|
});
|
|
57
63
|
});
|