@flighthq/lifecycle 0.5.0-next.1944.cd6b6e7 → 0.5.1-edge.638.d4dbd4e
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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lifecycle.d.ts +10 -14
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +24 -68
- package/dist/lifecycle.js.map +1 -1
- package/package.json +3 -3
- package/src/lifecycle.test.ts +111 -141
- package/src/lifecycleHost.test.ts +57 -0
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.
|
|
17
|
-
- [License](https://github.com/flighthq/flight/blob/
|
|
16
|
+
- [Source for @flighthq/lifecycle@0.5.1-edge.638.d4dbd4e](https://github.com/flighthq/flight/tree/d4dbd4e347dc8be10b180c85a766ab50333dcb51/packages/lifecycle)
|
|
17
|
+
- [License](https://github.com/flighthq/flight/blob/d4dbd4e347dc8be10b180c85a766ab50333dcb51/LICENSE.md)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { attachAppLifecycle, createAppLifecycle, detachAppLifecycle, disposeAppLifecycle, getAppLaunchKind, getAppLifecycleState, isAppActive, isAppBackground, isAppInactive, requestAppBack, } from './contract';
|
|
2
2
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,WAAW,EACX,eAAe,EACf,aAAa,EACb,cAAc,GACf,MAAM,YAAY,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { attachAppLifecycle, createAppLifecycle, detachAppLifecycle, disposeAppLifecycle, getAppLaunchKind, getAppLifecycleState, isAppActive, isAppBackground, isAppInactive, requestAppBack, } 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,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,kBAAkB,EAClB,mBAAmB,EACnB,gBAAgB,EAChB,oBAAoB,EACpB,WAAW,EACX,eAAe,EACf,aAAa,EACb,cAAc,GACf,MAAM,YAAY,CAAC"}
|
package/dist/lifecycle.d.ts
CHANGED
|
@@ -1,20 +1,16 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { AppLaunchKind, AppLifecycle, AppLifecycleState, LifecycleBackend } from '@flighthq/types/contract';
|
|
3
|
-
export declare function attachAppLifecycle(app: AppLifecycle): void;
|
|
1
|
+
import type { BackendOperationExplanation, LifecycleOperation } from '@flighthq/types/contract';
|
|
2
|
+
import type { AppLaunchKind, HasSystemLifecycle, AppLifecycle, AppLifecycleState, LifecycleBackend } from '@flighthq/types/contract';
|
|
3
|
+
export declare function attachAppLifecycle(host: HasSystemLifecycle, app: AppLifecycle): void;
|
|
4
4
|
export declare function createAppLifecycle(): AppLifecycle;
|
|
5
5
|
export declare function createWebLifecycleBackend(): LifecycleBackend;
|
|
6
6
|
export declare function detachAppLifecycle(app: AppLifecycle): void;
|
|
7
7
|
export declare function disposeAppLifecycle(app: AppLifecycle): void;
|
|
8
|
-
export declare function
|
|
9
|
-
export declare function getAppLaunchKind(): AppLaunchKind;
|
|
10
|
-
export declare function getAppLifecycleState(): AppLifecycleState;
|
|
11
|
-
export declare function
|
|
12
|
-
export declare function
|
|
13
|
-
export declare function
|
|
14
|
-
export declare function
|
|
15
|
-
export declare function isAppInactive(): boolean;
|
|
16
|
-
export declare function observeLifecycleHostResult(operation: string, succeeded: boolean): void;
|
|
8
|
+
export declare function explainLifecycleOperation(host: HasSystemLifecycle, operation: LifecycleOperation): BackendOperationExplanation;
|
|
9
|
+
export declare function getAppLaunchKind(host: HasSystemLifecycle): AppLaunchKind;
|
|
10
|
+
export declare function getAppLifecycleState(host: HasSystemLifecycle): AppLifecycleState;
|
|
11
|
+
export declare function hasLifecycleOperation(host: HasSystemLifecycle, operation: LifecycleOperation): boolean;
|
|
12
|
+
export declare function isAppActive(host: HasSystemLifecycle): boolean;
|
|
13
|
+
export declare function isAppBackground(host: HasSystemLifecycle): boolean;
|
|
14
|
+
export declare function isAppInactive(host: HasSystemLifecycle): boolean;
|
|
17
15
|
export declare function requestAppBack(app: AppLifecycle): boolean;
|
|
18
|
-
export declare function resetLifecycleBackendForTest(): void;
|
|
19
|
-
export declare function setLifecycleBackend(backend: LifecycleBackend | null): void;
|
|
20
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":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;
|
|
1
|
+
{"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../src/lifecycle.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,2BAA2B,EAAE,kBAAkB,EAAE,MAAM,0BAA0B,CAAC;AAChG,OAAO,KAAK,EACV,aAAa,EACb,kBAAkB,EAClB,YAAY,EACZ,iBAAiB,EAEjB,gBAAgB,EACjB,MAAM,0BAA0B,CAAC;AAYlC,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,kBAAkB,EAAE,GAAG,EAAE,YAAY,GAAG,IAAI,CAqCpF;AAGD,wBAAgB,kBAAkB,IAAI,YAAY,CAUjD;AAmBD,wBAAgB,yBAAyB,IAAI,gBAAgB,CA4E5D;AAGD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAM1D;AAID,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,YAAY,GAAG,IAAI,CAG3D;AAQD,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,kBAAkB,EACxB,SAAS,EAAE,kBAAkB,GAC5B,2BAA2B,CAG7B;AAMD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,kBAAkB,GAAG,aAAa,CAGxE;AAGD,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,kBAAkB,GAAG,iBAAiB,CAEhF;AAGD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,kBAAkB,EAAE,SAAS,EAAE,kBAAkB,GAAG,OAAO,CAEtG;AAGD,wBAAgB,WAAW,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAE7D;AAGD,wBAAgB,eAAe,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAEjE;AAID,wBAAgB,aAAa,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAE/D;AAOD,wBAAgB,cAAc,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAGzD"}
|
package/dist/lifecycle.js
CHANGED
|
@@ -9,9 +9,9 @@ import { createSignal, emitSignal } from '@flighthq/signals/contract';
|
|
|
9
9
|
// whether the derived state changed. onResume/onPause are deduped edges: 'active'→non-'active'
|
|
10
10
|
// fires onPause (including the interruption edge 'active'→'inactive'); non-'active'→'active' fires
|
|
11
11
|
// onResume. The 'inactive'→'background' and reverse transitions do not fire onPause/onResume again.
|
|
12
|
-
export function attachAppLifecycle(app) {
|
|
12
|
+
export function attachAppLifecycle(host, app) {
|
|
13
13
|
detachAppLifecycle(app);
|
|
14
|
-
const backend =
|
|
14
|
+
const backend = host.system.lifecycle;
|
|
15
15
|
let previous = backend.getState();
|
|
16
16
|
const unsubscribeState = backend.subscribe(() => {
|
|
17
17
|
const state = backend.getState();
|
|
@@ -173,61 +173,44 @@ export function disposeAppLifecycle(app) {
|
|
|
173
173
|
detachAppLifecycle(app);
|
|
174
174
|
_savedState.delete(app);
|
|
175
175
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
viability: _hostObservation !== null ? _hostObservation.viability : 'unobserved',
|
|
186
|
-
};
|
|
187
|
-
}
|
|
188
|
-
return { conflict: false, layer: 'host-not-enabled', operation: null, viability: 'unobserved' };
|
|
176
|
+
// Which layer implements `operation`, and whether anything real does — the per-operation answer
|
|
177
|
+
// `explainLifecycleBackend` cannot give: that one reports a backend is installed, this one reports whether
|
|
178
|
+
// THIS operation on it is a genuine implementation or the sentinel standing in.
|
|
179
|
+
//
|
|
180
|
+
// ★ The sentinel is deliberately not consulted. It answers every operation, so counting it would make
|
|
181
|
+
// this report `true` for everything and say nothing at all.
|
|
182
|
+
export function explainLifecycleOperation(host, operation) {
|
|
183
|
+
const implemented = typeof host.system.lifecycle[operation] === 'function';
|
|
184
|
+
return { implemented, layer: implemented ? 'host' : 'sentinel', operation };
|
|
189
185
|
}
|
|
190
186
|
// Returns the kind of launch — 'cold' (fresh process) or 'warm' (resumed from background). The web
|
|
191
187
|
// backend approximates this via PerformanceNavigationTiming.type ('back_forward' → 'warm', all
|
|
192
188
|
// others → 'cold'). Returns 'warm' as a safe fallback when the backend does not implement
|
|
193
189
|
// getLaunchKind (legacy or minimal backends that pre-date the optional method).
|
|
194
|
-
export function getAppLaunchKind() {
|
|
195
|
-
const backend =
|
|
190
|
+
export function getAppLaunchKind(host) {
|
|
191
|
+
const backend = host.system.lifecycle;
|
|
196
192
|
return backend.getLaunchKind !== undefined ? backend.getLaunchKind() : 'warm';
|
|
197
193
|
}
|
|
198
194
|
// Returns the current application lifecycle state from the active backend.
|
|
199
|
-
export function getAppLifecycleState() {
|
|
200
|
-
return
|
|
195
|
+
export function getAppLifecycleState(host) {
|
|
196
|
+
return host.system.lifecycle.getState();
|
|
201
197
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
export function installLifecycleHostBackend(backend) {
|
|
206
|
-
if (_host !== null) {
|
|
207
|
-
if (_host !== backend)
|
|
208
|
-
_hostConflict = true;
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
_host = backend;
|
|
198
|
+
// Whether a real backend implements `operation`, as opposed to the sentinel answering for it.
|
|
199
|
+
export function hasLifecycleOperation(host, operation) {
|
|
200
|
+
return explainLifecycleOperation(host, operation).implemented;
|
|
212
201
|
}
|
|
213
202
|
// Returns true when the application is in the 'active' state (visible and focused).
|
|
214
|
-
export function isAppActive() {
|
|
215
|
-
return
|
|
203
|
+
export function isAppActive(host) {
|
|
204
|
+
return host.system.lifecycle.getState() === 'active';
|
|
216
205
|
}
|
|
217
206
|
// Returns true when the application is in the 'background' state (hidden/suspended).
|
|
218
|
-
export function isAppBackground() {
|
|
219
|
-
return
|
|
207
|
+
export function isAppBackground(host) {
|
|
208
|
+
return host.system.lifecycle.getState() === 'background';
|
|
220
209
|
}
|
|
221
210
|
// Returns true when the application is in the 'inactive' state (visible but not focused —
|
|
222
211
|
// e.g. app switcher, control-center overlay, incoming call).
|
|
223
|
-
export function isAppInactive() {
|
|
224
|
-
return
|
|
225
|
-
}
|
|
226
|
-
export function observeLifecycleHostResult(operation, succeeded) {
|
|
227
|
-
_hostObservation = {
|
|
228
|
-
operation,
|
|
229
|
-
viability: succeeded ? 'available' : 'runtime-api-unavailable',
|
|
230
|
-
};
|
|
212
|
+
export function isAppInactive(host) {
|
|
213
|
+
return host.system.lifecycle.getState() === 'inactive';
|
|
231
214
|
}
|
|
232
215
|
// Emits app.onBackButton and returns whether the back action may proceed. Returns false when a
|
|
233
216
|
// listener vetoed by calling cancelSignal(app.onBackButton), meaning the listener handled
|
|
@@ -238,33 +221,6 @@ export function requestAppBack(app) {
|
|
|
238
221
|
emitSignal(app.onBackButton);
|
|
239
222
|
return app.onBackButton.data?.cancelled !== true;
|
|
240
223
|
}
|
|
241
|
-
export function resetLifecycleBackendForTest() {
|
|
242
|
-
_custom = null;
|
|
243
|
-
_host = null;
|
|
244
|
-
_hostConflict = false;
|
|
245
|
-
_hostObservation = null;
|
|
246
|
-
}
|
|
247
|
-
export function setLifecycleBackend(backend) {
|
|
248
|
-
_custom = backend;
|
|
249
|
-
}
|
|
250
224
|
const _savedState = new WeakMap();
|
|
251
|
-
const _sentinel = {
|
|
252
|
-
getState() {
|
|
253
|
-
return 'active';
|
|
254
|
-
},
|
|
255
|
-
getLaunchKind() {
|
|
256
|
-
return 'cold';
|
|
257
|
-
},
|
|
258
|
-
subscribe() {
|
|
259
|
-
return () => { };
|
|
260
|
-
},
|
|
261
|
-
subscribeMemoryWarning() {
|
|
262
|
-
return () => { };
|
|
263
|
-
},
|
|
264
|
-
};
|
|
265
225
|
const _subscriptions = new WeakMap();
|
|
266
|
-
let _custom = null;
|
|
267
|
-
let _host = null;
|
|
268
|
-
let _hostConflict = false;
|
|
269
|
-
let _hostObservation = null;
|
|
270
226
|
//# sourceMappingURL=lifecycle.js.map
|
package/dist/lifecycle.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../src/lifecycle.ts"],"names":[],"mappings":"AAAA,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.
|
|
3
|
+
"version": "0.5.1-edge.638.d4dbd4e",
|
|
4
4
|
"author": "Joshua Granick and other contributors",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"clean:dist": "tsx ../../scripts/clean-package-dist.ts"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@flighthq/signals": "0.5.
|
|
42
|
-
"@flighthq/types": "0.5.
|
|
41
|
+
"@flighthq/signals": "0.5.1-edge.638.d4dbd4e",
|
|
42
|
+
"@flighthq/types": "0.5.1-edge.638.d4dbd4e"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"typescript": "^5.3.0"
|
package/src/lifecycle.test.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { cancelSignal, connectSignal } from '@flighthq/signals/contract';
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
HasSystemLifecycle,
|
|
4
|
+
AppLifecycleState,
|
|
5
|
+
AppMemoryPressure,
|
|
6
|
+
LifecycleBackend,
|
|
7
|
+
LifecycleOperation,
|
|
8
|
+
} from '@flighthq/types/contract';
|
|
3
9
|
|
|
4
10
|
import {
|
|
5
11
|
attachAppLifecycle,
|
|
@@ -7,18 +13,14 @@ import {
|
|
|
7
13
|
createWebLifecycleBackend,
|
|
8
14
|
detachAppLifecycle,
|
|
9
15
|
disposeAppLifecycle,
|
|
16
|
+
explainLifecycleOperation,
|
|
10
17
|
getAppLaunchKind,
|
|
11
18
|
getAppLifecycleState,
|
|
12
|
-
|
|
19
|
+
hasLifecycleOperation,
|
|
13
20
|
isAppActive,
|
|
14
21
|
isAppBackground,
|
|
15
22
|
isAppInactive,
|
|
16
23
|
requestAppBack,
|
|
17
|
-
setLifecycleBackend,
|
|
18
|
-
explainLifecycleBackend,
|
|
19
|
-
installLifecycleHostBackend,
|
|
20
|
-
observeLifecycleHostResult,
|
|
21
|
-
resetLifecycleBackendForTest,
|
|
22
24
|
} from './lifecycle';
|
|
23
25
|
|
|
24
26
|
type FakeBackend = LifecycleBackend & {
|
|
@@ -27,6 +29,10 @@ type FakeBackend = LifecycleBackend & {
|
|
|
27
29
|
fireMemory: (level: AppMemoryPressure) => void;
|
|
28
30
|
};
|
|
29
31
|
|
|
32
|
+
function hostOf(backend: LifecycleBackend): HasSystemLifecycle {
|
|
33
|
+
return { system: { lifecycle: backend } } as HasSystemLifecycle;
|
|
34
|
+
}
|
|
35
|
+
|
|
30
36
|
function fakeBackend(): FakeBackend {
|
|
31
37
|
let stateListener: (() => void) | null = null;
|
|
32
38
|
let memoryListener: ((level: AppMemoryPressure) => void) | null = null;
|
|
@@ -56,18 +62,16 @@ function fakeBackend(): FakeBackend {
|
|
|
56
62
|
};
|
|
57
63
|
}
|
|
58
64
|
|
|
59
|
-
afterEach(() => setLifecycleBackend(null));
|
|
60
|
-
|
|
61
65
|
describe('attachAppLifecycle', () => {
|
|
62
66
|
it('emits onPause and onStateChange when leaving active for background', () => {
|
|
63
67
|
const backend = fakeBackend();
|
|
64
|
-
|
|
68
|
+
const host = hostOf(backend);
|
|
65
69
|
const app = createAppLifecycle();
|
|
66
70
|
let changes = 0;
|
|
67
71
|
let pauses = 0;
|
|
68
72
|
connectSignal(app.onStateChange, () => changes++);
|
|
69
73
|
connectSignal(app.onPause, () => pauses++);
|
|
70
|
-
attachAppLifecycle(app);
|
|
74
|
+
attachAppLifecycle(host, app);
|
|
71
75
|
backend.state = 'background';
|
|
72
76
|
backend.fire();
|
|
73
77
|
expect(changes).toBe(1);
|
|
@@ -76,11 +80,11 @@ describe('attachAppLifecycle', () => {
|
|
|
76
80
|
|
|
77
81
|
it('emits onPause when leaving active for inactive', () => {
|
|
78
82
|
const backend = fakeBackend();
|
|
79
|
-
|
|
83
|
+
const host = hostOf(backend);
|
|
80
84
|
const app = createAppLifecycle();
|
|
81
85
|
let pauses = 0;
|
|
82
86
|
connectSignal(app.onPause, () => pauses++);
|
|
83
|
-
attachAppLifecycle(app);
|
|
87
|
+
attachAppLifecycle(host, app);
|
|
84
88
|
backend.state = 'inactive';
|
|
85
89
|
backend.fire();
|
|
86
90
|
expect(pauses).toBe(1);
|
|
@@ -89,11 +93,11 @@ describe('attachAppLifecycle', () => {
|
|
|
89
93
|
it('does not double-fire onPause for inactive → background', () => {
|
|
90
94
|
const backend = fakeBackend();
|
|
91
95
|
backend.state = 'inactive';
|
|
92
|
-
|
|
96
|
+
const host = hostOf(backend);
|
|
93
97
|
const app = createAppLifecycle();
|
|
94
98
|
let pauses = 0;
|
|
95
99
|
connectSignal(app.onPause, () => pauses++);
|
|
96
|
-
attachAppLifecycle(app);
|
|
100
|
+
attachAppLifecycle(host, app);
|
|
97
101
|
backend.state = 'background';
|
|
98
102
|
backend.fire();
|
|
99
103
|
// inactive → background should not re-fire onPause (already paused)
|
|
@@ -103,11 +107,11 @@ describe('attachAppLifecycle', () => {
|
|
|
103
107
|
it('emits onResume when returning to active from background', () => {
|
|
104
108
|
const backend = fakeBackend();
|
|
105
109
|
backend.state = 'background';
|
|
106
|
-
|
|
110
|
+
const host = hostOf(backend);
|
|
107
111
|
const app = createAppLifecycle();
|
|
108
112
|
let resumes = 0;
|
|
109
113
|
connectSignal(app.onResume, () => resumes++);
|
|
110
|
-
attachAppLifecycle(app);
|
|
114
|
+
attachAppLifecycle(host, app);
|
|
111
115
|
backend.state = 'active';
|
|
112
116
|
backend.fire();
|
|
113
117
|
expect(resumes).toBe(1);
|
|
@@ -116,11 +120,11 @@ describe('attachAppLifecycle', () => {
|
|
|
116
120
|
it('emits onResume when returning to active from inactive', () => {
|
|
117
121
|
const backend = fakeBackend();
|
|
118
122
|
backend.state = 'inactive';
|
|
119
|
-
|
|
123
|
+
const host = hostOf(backend);
|
|
120
124
|
const app = createAppLifecycle();
|
|
121
125
|
let resumes = 0;
|
|
122
126
|
connectSignal(app.onResume, () => resumes++);
|
|
123
|
-
attachAppLifecycle(app);
|
|
127
|
+
attachAppLifecycle(host, app);
|
|
124
128
|
backend.state = 'active';
|
|
125
129
|
backend.fire();
|
|
126
130
|
expect(resumes).toBe(1);
|
|
@@ -128,12 +132,12 @@ describe('attachAppLifecycle', () => {
|
|
|
128
132
|
|
|
129
133
|
it('is idempotent — re-attaching tears down the prior subscription', () => {
|
|
130
134
|
const backend = fakeBackend();
|
|
131
|
-
|
|
135
|
+
const host = hostOf(backend);
|
|
132
136
|
const app = createAppLifecycle();
|
|
133
137
|
let changes = 0;
|
|
134
138
|
connectSignal(app.onStateChange, () => changes++);
|
|
135
|
-
attachAppLifecycle(app);
|
|
136
|
-
attachAppLifecycle(app);
|
|
139
|
+
attachAppLifecycle(host, app);
|
|
140
|
+
attachAppLifecycle(host, app);
|
|
137
141
|
backend.state = 'background';
|
|
138
142
|
backend.fire();
|
|
139
143
|
// Only one subscription should be active.
|
|
@@ -142,14 +146,14 @@ describe('attachAppLifecycle', () => {
|
|
|
142
146
|
|
|
143
147
|
it('emits onSaveState when leaving active', () => {
|
|
144
148
|
const backend = fakeBackend();
|
|
145
|
-
|
|
149
|
+
const host = hostOf(backend);
|
|
146
150
|
const app = createAppLifecycle();
|
|
147
151
|
let saved = false;
|
|
148
152
|
connectSignal(app.onSaveState, (bag) => {
|
|
149
153
|
bag['key'] = 'value';
|
|
150
154
|
saved = true;
|
|
151
155
|
});
|
|
152
|
-
attachAppLifecycle(app);
|
|
156
|
+
attachAppLifecycle(host, app);
|
|
153
157
|
backend.state = 'background';
|
|
154
158
|
backend.fire();
|
|
155
159
|
expect(saved).toBe(true);
|
|
@@ -157,7 +161,7 @@ describe('attachAppLifecycle', () => {
|
|
|
157
161
|
|
|
158
162
|
it('emits onRestoreState with saved bag when returning to active', () => {
|
|
159
163
|
const backend = fakeBackend();
|
|
160
|
-
|
|
164
|
+
const host = hostOf(backend);
|
|
161
165
|
const app = createAppLifecycle();
|
|
162
166
|
connectSignal(app.onSaveState, (bag) => {
|
|
163
167
|
bag['x'] = 42;
|
|
@@ -166,7 +170,7 @@ describe('attachAppLifecycle', () => {
|
|
|
166
170
|
connectSignal(app.onRestoreState, (state) => {
|
|
167
171
|
restored = state;
|
|
168
172
|
});
|
|
169
|
-
attachAppLifecycle(app);
|
|
173
|
+
attachAppLifecycle(host, app);
|
|
170
174
|
// Transition away to trigger save.
|
|
171
175
|
backend.state = 'background';
|
|
172
176
|
backend.fire();
|
|
@@ -179,11 +183,11 @@ describe('attachAppLifecycle', () => {
|
|
|
179
183
|
|
|
180
184
|
it('subscribes to memory warnings when backend supports it', () => {
|
|
181
185
|
const backend = fakeBackend();
|
|
182
|
-
|
|
186
|
+
const host = hostOf(backend);
|
|
183
187
|
const app = createAppLifecycle();
|
|
184
188
|
const levels: AppMemoryPressure[] = [];
|
|
185
189
|
connectSignal(app.onMemoryWarning, (level) => levels.push(level));
|
|
186
|
-
attachAppLifecycle(app);
|
|
190
|
+
attachAppLifecycle(host, app);
|
|
187
191
|
backend.fireMemory('critical');
|
|
188
192
|
expect(levels).toEqual(['critical']);
|
|
189
193
|
});
|
|
@@ -197,11 +201,11 @@ describe('attachAppLifecycle', () => {
|
|
|
197
201
|
const states: AppLifecycleState[] = ['active', 'inactive', 'background'];
|
|
198
202
|
for (let trial = 0; trial < 200; trial++) {
|
|
199
203
|
const backend = fakeBackend();
|
|
200
|
-
|
|
204
|
+
const host = hostOf(backend);
|
|
201
205
|
const app = createAppLifecycle();
|
|
202
206
|
let changes = 0;
|
|
203
207
|
connectSignal(app.onStateChange, () => changes++);
|
|
204
|
-
attachAppLifecycle(app);
|
|
208
|
+
attachAppLifecycle(host, app);
|
|
205
209
|
const fireCount = 1 + Math.floor(Math.random() * 30);
|
|
206
210
|
for (let i = 0; i < fireCount; i++) {
|
|
207
211
|
backend.state = states[Math.floor(Math.random() * states.length)];
|
|
@@ -219,13 +223,13 @@ describe('attachAppLifecycle', () => {
|
|
|
219
223
|
const backend = fakeBackend();
|
|
220
224
|
const start = states[Math.floor(Math.random() * states.length)];
|
|
221
225
|
backend.state = start;
|
|
222
|
-
|
|
226
|
+
const host = hostOf(backend);
|
|
223
227
|
const app = createAppLifecycle();
|
|
224
228
|
let pauses = 0;
|
|
225
229
|
let resumes = 0;
|
|
226
230
|
connectSignal(app.onPause, () => pauses++);
|
|
227
231
|
connectSignal(app.onResume, () => resumes++);
|
|
228
|
-
attachAppLifecycle(app);
|
|
232
|
+
attachAppLifecycle(host, app);
|
|
229
233
|
|
|
230
234
|
// Drive a random storm while computing the expected edge counts from an independent model:
|
|
231
235
|
// a pause edge is every 'active' → non-'active' transition, a resume edge every
|
|
@@ -255,7 +259,7 @@ describe('attachAppLifecycle', () => {
|
|
|
255
259
|
for (let trial = 0; trial < 100; trial++) {
|
|
256
260
|
const backend = fakeBackend();
|
|
257
261
|
backend.state = 'active';
|
|
258
|
-
|
|
262
|
+
const host = hostOf(backend);
|
|
259
263
|
const app = createAppLifecycle();
|
|
260
264
|
let changes = 0;
|
|
261
265
|
let pauses = 0;
|
|
@@ -263,7 +267,7 @@ describe('attachAppLifecycle', () => {
|
|
|
263
267
|
connectSignal(app.onStateChange, () => changes++);
|
|
264
268
|
connectSignal(app.onPause, () => pauses++);
|
|
265
269
|
connectSignal(app.onResume, () => resumes++);
|
|
266
|
-
attachAppLifecycle(app);
|
|
270
|
+
attachAppLifecycle(host, app);
|
|
267
271
|
const fireCount = 1 + Math.floor(Math.random() * 30);
|
|
268
272
|
for (let i = 0; i < fireCount; i++) backend.fire();
|
|
269
273
|
expect(changes).toBe(fireCount);
|
|
@@ -282,13 +286,13 @@ describe('attachAppLifecycle', () => {
|
|
|
282
286
|
for (let trial = 0; trial < 200; trial++) {
|
|
283
287
|
const backend = fakeBackend();
|
|
284
288
|
backend.state = states[Math.floor(Math.random() * states.length)];
|
|
285
|
-
|
|
289
|
+
const host = hostOf(backend);
|
|
286
290
|
const app = createAppLifecycle();
|
|
287
291
|
let pauses = 0;
|
|
288
292
|
let resumes = 0;
|
|
289
293
|
connectSignal(app.onPause, () => pauses++);
|
|
290
294
|
connectSignal(app.onResume, () => resumes++);
|
|
291
|
-
attachAppLifecycle(app);
|
|
295
|
+
attachAppLifecycle(host, app);
|
|
292
296
|
const fireCount = 1 + Math.floor(Math.random() * 30);
|
|
293
297
|
for (let i = 0; i < fireCount; i++) {
|
|
294
298
|
backend.state = states[Math.floor(Math.random() * states.length)];
|
|
@@ -398,11 +402,11 @@ describe('createWebLifecycleBackend', () => {
|
|
|
398
402
|
describe('detachAppLifecycle', () => {
|
|
399
403
|
it('stops further delivery', () => {
|
|
400
404
|
const backend = fakeBackend();
|
|
401
|
-
|
|
405
|
+
const host = hostOf(backend);
|
|
402
406
|
const app = createAppLifecycle();
|
|
403
407
|
let changes = 0;
|
|
404
408
|
connectSignal(app.onStateChange, () => changes++);
|
|
405
|
-
attachAppLifecycle(app);
|
|
409
|
+
attachAppLifecycle(host, app);
|
|
406
410
|
detachAppLifecycle(app);
|
|
407
411
|
backend.fire();
|
|
408
412
|
expect(changes).toBe(0);
|
|
@@ -417,58 +421,62 @@ describe('detachAppLifecycle', () => {
|
|
|
417
421
|
describe('disposeAppLifecycle', () => {
|
|
418
422
|
it('detaches the subscription', () => {
|
|
419
423
|
const backend = fakeBackend();
|
|
420
|
-
|
|
424
|
+
const host = hostOf(backend);
|
|
421
425
|
const app = createAppLifecycle();
|
|
422
|
-
attachAppLifecycle(app);
|
|
426
|
+
attachAppLifecycle(host, app);
|
|
423
427
|
expect(() => disposeAppLifecycle(app)).not.toThrow();
|
|
424
428
|
});
|
|
425
429
|
|
|
426
430
|
it('stops delivery after dispose', () => {
|
|
427
431
|
const backend = fakeBackend();
|
|
428
|
-
|
|
432
|
+
const host = hostOf(backend);
|
|
429
433
|
const app = createAppLifecycle();
|
|
430
434
|
let changes = 0;
|
|
431
435
|
connectSignal(app.onStateChange, () => changes++);
|
|
432
|
-
attachAppLifecycle(app);
|
|
436
|
+
attachAppLifecycle(host, app);
|
|
433
437
|
disposeAppLifecycle(app);
|
|
434
438
|
backend.fire();
|
|
435
439
|
expect(changes).toBe(0);
|
|
436
440
|
});
|
|
437
441
|
});
|
|
438
442
|
|
|
439
|
-
describe('
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
const
|
|
445
|
-
expect(
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
it('reports custom layer when a custom backend is set', () => {
|
|
451
|
-
setLifecycleBackend(fakeBackend());
|
|
452
|
-
expect(explainLifecycleBackend().layer).toBe('custom');
|
|
443
|
+
describe('explainLifecycleOperation', () => {
|
|
444
|
+
// The sentinel is gone with the ambient resolver: a host either carries a provider that implements
|
|
445
|
+
// the operation or it does not, and "not implemented" is now a fact about THAT host rather than
|
|
446
|
+
// about a process-wide fallback that answered everything.
|
|
447
|
+
it('reports the host provider as implementing what it provides', () => {
|
|
448
|
+
const host = hostOf(fakeBackend());
|
|
449
|
+
expect(explainLifecycleOperation(host, 'getState')).toEqual({
|
|
450
|
+
implemented: true,
|
|
451
|
+
layer: 'host',
|
|
452
|
+
operation: 'getState',
|
|
453
|
+
});
|
|
453
454
|
});
|
|
454
455
|
|
|
455
|
-
it('reports
|
|
456
|
-
|
|
457
|
-
expect(
|
|
456
|
+
it('reports an operation the host provider omits as unimplemented', () => {
|
|
457
|
+
const host = hostOf({ getState: () => 'active', subscribe: () => () => {} } as LifecycleBackend);
|
|
458
|
+
expect(explainLifecycleOperation(host, 'getLaunchKind')).toEqual({
|
|
459
|
+
implemented: false,
|
|
460
|
+
layer: 'sentinel',
|
|
461
|
+
operation: 'getLaunchKind',
|
|
462
|
+
});
|
|
458
463
|
});
|
|
459
464
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
465
|
+
// Two hosts, two answers for the same operation — impossible before the migration, because one
|
|
466
|
+
// process-wide backend answered for every caller.
|
|
467
|
+
it('answers per host rather than process-wide', () => {
|
|
468
|
+
const rich = hostOf(fakeBackend());
|
|
469
|
+
const bare = hostOf({ getState: () => 'active', subscribe: () => () => {} } as LifecycleBackend);
|
|
470
|
+
expect(explainLifecycleOperation(rich, 'subscribeMemoryWarning').implemented).toBe(true);
|
|
471
|
+
expect(explainLifecycleOperation(bare, 'subscribeMemoryWarning').implemented).toBe(false);
|
|
464
472
|
});
|
|
465
473
|
});
|
|
466
474
|
|
|
467
475
|
describe('getAppLaunchKind', () => {
|
|
468
476
|
it("returns 'warm' when backend does not implement getLaunchKind", () => {
|
|
469
477
|
const backend = fakeBackend();
|
|
470
|
-
|
|
471
|
-
expect(getAppLaunchKind()).toBe('warm');
|
|
478
|
+
const host = hostOf(backend);
|
|
479
|
+
expect(getAppLaunchKind(host)).toBe('warm');
|
|
472
480
|
});
|
|
473
481
|
|
|
474
482
|
it('delegates to backend.getLaunchKind when present', () => {
|
|
@@ -477,8 +485,8 @@ describe('getAppLaunchKind', () => {
|
|
|
477
485
|
subscribe: () => () => {},
|
|
478
486
|
getLaunchKind: () => 'cold',
|
|
479
487
|
};
|
|
480
|
-
|
|
481
|
-
expect(getAppLaunchKind()).toBe('cold');
|
|
488
|
+
const host = hostOf(backend);
|
|
489
|
+
expect(getAppLaunchKind(host)).toBe('cold');
|
|
482
490
|
});
|
|
483
491
|
});
|
|
484
492
|
|
|
@@ -486,33 +494,20 @@ describe('getAppLifecycleState', () => {
|
|
|
486
494
|
it('reads from the active backend', () => {
|
|
487
495
|
const backend = fakeBackend();
|
|
488
496
|
backend.state = 'inactive';
|
|
489
|
-
|
|
490
|
-
expect(getAppLifecycleState()).toBe('inactive');
|
|
497
|
+
const host = hostOf(backend);
|
|
498
|
+
expect(getAppLifecycleState(host)).toBe('inactive');
|
|
491
499
|
});
|
|
492
500
|
});
|
|
493
501
|
|
|
494
|
-
describe('
|
|
495
|
-
it('
|
|
496
|
-
|
|
502
|
+
describe('hasLifecycleOperation', () => {
|
|
503
|
+
it('agrees with explainLifecycleOperation for the same host', () => {
|
|
504
|
+
const host = hostOf(fakeBackend());
|
|
505
|
+
expect(hasLifecycleOperation(host, 'getState')).toBe(explainLifecycleOperation(host, 'getState').implemented);
|
|
497
506
|
});
|
|
498
|
-
});
|
|
499
|
-
|
|
500
|
-
describe('installLifecycleHostBackend', () => {
|
|
501
|
-
afterEach(() => resetLifecycleBackendForTest());
|
|
502
507
|
|
|
503
|
-
it('
|
|
504
|
-
const
|
|
505
|
-
|
|
506
|
-
expect(getLifecycleBackend()).toBe(backend);
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
it('is first-host-wins: a second different backend sets conflict', () => {
|
|
510
|
-
const first = fakeBackend();
|
|
511
|
-
const second = fakeBackend();
|
|
512
|
-
installLifecycleHostBackend(first);
|
|
513
|
-
installLifecycleHostBackend(second);
|
|
514
|
-
expect(getLifecycleBackend()).toBe(first);
|
|
515
|
-
expect(explainLifecycleBackend().conflict).toBe(true);
|
|
508
|
+
it('is false for an operation the host provider omits', () => {
|
|
509
|
+
const host = hostOf({ getState: () => 'active', subscribe: () => () => {} } as LifecycleBackend);
|
|
510
|
+
expect(hasLifecycleOperation(host, 'getLaunchKind')).toBe(false);
|
|
516
511
|
});
|
|
517
512
|
});
|
|
518
513
|
|
|
@@ -520,22 +515,22 @@ describe('isAppActive', () => {
|
|
|
520
515
|
it("returns true when state is 'active'", () => {
|
|
521
516
|
const backend = fakeBackend();
|
|
522
517
|
backend.state = 'active';
|
|
523
|
-
|
|
524
|
-
expect(isAppActive()).toBe(true);
|
|
518
|
+
const host = hostOf(backend);
|
|
519
|
+
expect(isAppActive(host)).toBe(true);
|
|
525
520
|
});
|
|
526
521
|
|
|
527
522
|
it("returns false when state is 'background'", () => {
|
|
528
523
|
const backend = fakeBackend();
|
|
529
524
|
backend.state = 'background';
|
|
530
|
-
|
|
531
|
-
expect(isAppActive()).toBe(false);
|
|
525
|
+
const host = hostOf(backend);
|
|
526
|
+
expect(isAppActive(host)).toBe(false);
|
|
532
527
|
});
|
|
533
528
|
|
|
534
529
|
it("returns false when state is 'inactive'", () => {
|
|
535
530
|
const backend = fakeBackend();
|
|
536
531
|
backend.state = 'inactive';
|
|
537
|
-
|
|
538
|
-
expect(isAppActive()).toBe(false);
|
|
532
|
+
const host = hostOf(backend);
|
|
533
|
+
expect(isAppActive(host)).toBe(false);
|
|
539
534
|
});
|
|
540
535
|
});
|
|
541
536
|
|
|
@@ -543,49 +538,44 @@ describe('isAppBackground', () => {
|
|
|
543
538
|
it("returns true when state is 'background'", () => {
|
|
544
539
|
const backend = fakeBackend();
|
|
545
540
|
backend.state = 'background';
|
|
546
|
-
|
|
547
|
-
expect(isAppBackground()).toBe(true);
|
|
541
|
+
const host = hostOf(backend);
|
|
542
|
+
expect(isAppBackground(host)).toBe(true);
|
|
548
543
|
});
|
|
549
544
|
|
|
550
545
|
it("returns false when state is 'active'", () => {
|
|
551
546
|
const backend = fakeBackend();
|
|
552
547
|
backend.state = 'active';
|
|
553
|
-
|
|
554
|
-
expect(isAppBackground()).toBe(false);
|
|
548
|
+
const host = hostOf(backend);
|
|
549
|
+
expect(isAppBackground(host)).toBe(false);
|
|
555
550
|
});
|
|
556
551
|
});
|
|
557
552
|
|
|
553
|
+
// Per-operation availability for LifecycleBackend. The operations below are the ones the interface declares
|
|
554
|
+
// OPTIONAL, so a host that omits them is compliant rather than broken — that is the absence-of-an-export
|
|
555
|
+
// ruling, and this is the query that makes it observable.
|
|
556
|
+
const OPTIONAL_OPERATIONS: readonly LifecycleOperation[] = ['getLaunchKind', 'subscribeMemoryWarning'];
|
|
557
|
+
|
|
558
|
+
// A host implementing only the REQUIRED members — partial support declared by absence.
|
|
559
|
+
function partialBackend(): LifecycleBackend {
|
|
560
|
+
return {
|
|
561
|
+
getState: (() => undefined) as never,
|
|
562
|
+
subscribe: (() => undefined) as never,
|
|
563
|
+
} as LifecycleBackend;
|
|
564
|
+
}
|
|
565
|
+
|
|
558
566
|
describe('isAppInactive', () => {
|
|
559
567
|
it("returns true when state is 'inactive'", () => {
|
|
560
568
|
const backend = fakeBackend();
|
|
561
569
|
backend.state = 'inactive';
|
|
562
|
-
|
|
563
|
-
expect(isAppInactive()).toBe(true);
|
|
570
|
+
const host = hostOf(backend);
|
|
571
|
+
expect(isAppInactive(host)).toBe(true);
|
|
564
572
|
});
|
|
565
573
|
|
|
566
574
|
it("returns false when state is 'active'", () => {
|
|
567
575
|
const backend = fakeBackend();
|
|
568
576
|
backend.state = 'active';
|
|
569
|
-
|
|
570
|
-
expect(isAppInactive()).toBe(false);
|
|
571
|
-
});
|
|
572
|
-
});
|
|
573
|
-
|
|
574
|
-
describe('observeLifecycleHostResult', () => {
|
|
575
|
-
afterEach(() => resetLifecycleBackendForTest());
|
|
576
|
-
|
|
577
|
-
it('records a successful observation', () => {
|
|
578
|
-
installLifecycleHostBackend(fakeBackend());
|
|
579
|
-
observeLifecycleHostResult('getState', true);
|
|
580
|
-
const explanation = explainLifecycleBackend();
|
|
581
|
-
expect(explanation.operation).toBe('getState');
|
|
582
|
-
expect(explanation.viability).toBe('available');
|
|
583
|
-
});
|
|
584
|
-
|
|
585
|
-
it('records a failed observation', () => {
|
|
586
|
-
installLifecycleHostBackend(fakeBackend());
|
|
587
|
-
observeLifecycleHostResult('getState', false);
|
|
588
|
-
expect(explainLifecycleBackend().viability).toBe('runtime-api-unavailable');
|
|
577
|
+
const host = hostOf(backend);
|
|
578
|
+
expect(isAppInactive(host)).toBe(false);
|
|
589
579
|
});
|
|
590
580
|
});
|
|
591
581
|
|
|
@@ -609,23 +599,3 @@ describe('requestAppBack', () => {
|
|
|
609
599
|
expect(fired).toBe(1);
|
|
610
600
|
});
|
|
611
601
|
});
|
|
612
|
-
|
|
613
|
-
describe('resetLifecycleBackendForTest', () => {
|
|
614
|
-
it('clears all backend slots', () => {
|
|
615
|
-
setLifecycleBackend(fakeBackend());
|
|
616
|
-
installLifecycleHostBackend(fakeBackend());
|
|
617
|
-
observeLifecycleHostResult('getState', true);
|
|
618
|
-
resetLifecycleBackendForTest();
|
|
619
|
-
expect(explainLifecycleBackend().layer).toBe('host-not-enabled');
|
|
620
|
-
expect(explainLifecycleBackend().conflict).toBe(false);
|
|
621
|
-
expect(explainLifecycleBackend().viability).toBe('unobserved');
|
|
622
|
-
});
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
describe('setLifecycleBackend', () => {
|
|
626
|
-
it('clears back to the web fallback when passed null', () => {
|
|
627
|
-
setLifecycleBackend(fakeBackend());
|
|
628
|
-
setLifecycleBackend(null);
|
|
629
|
-
expect(getLifecycleBackend()).not.toBeNull();
|
|
630
|
-
});
|
|
631
|
-
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { AppLifecycleState, HasSystemLifecycle, LifecycleBackend } from '@flighthq/types/contract';
|
|
2
|
+
|
|
3
|
+
import { getAppLaunchKind, getAppLifecycleState, isAppActive, isAppBackground, isAppInactive } from './lifecycle';
|
|
4
|
+
|
|
5
|
+
function hostWith(backend: Partial<LifecycleBackend>): HasSystemLifecycle {
|
|
6
|
+
return { system: { lifecycle: backend as LifecycleBackend } } as HasSystemLifecycle;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function stateHost(state: AppLifecycleState): HasSystemLifecycle {
|
|
10
|
+
return hostWith({ getState: () => state, subscribe: () => () => {} });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
describe('getAppLaunchKind', () => {
|
|
14
|
+
it('reads the launch kind from the host it is given', () => {
|
|
15
|
+
expect(getAppLaunchKind(hostWith({ getState: () => 'active', getLaunchKind: () => 'warm' }))).toBe('warm');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// getLaunchKind is optional on the backend; a provider without it must not throw.
|
|
19
|
+
it('falls back to warm when the provider does not implement it', () => {
|
|
20
|
+
expect(getAppLaunchKind(stateHost('active'))).toBe('warm');
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe('getAppLifecycleState', () => {
|
|
25
|
+
it('reads the state from the host it is given', () => {
|
|
26
|
+
expect(getAppLifecycleState(stateHost('background'))).toBe('background');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Two hosts, two answers, no ambient selection in between. This is the property the migration is
|
|
30
|
+
// for: before it, both calls resolved one process-wide backend and the second host was unreachable.
|
|
31
|
+
it('keeps two hosts independent', () => {
|
|
32
|
+
expect(getAppLifecycleState(stateHost('active'))).toBe('active');
|
|
33
|
+
expect(getAppLifecycleState(stateHost('inactive'))).toBe('inactive');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('isAppActive', () => {
|
|
38
|
+
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);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('isAppBackground', () => {
|
|
46
|
+
it('reports background only for the background state', () => {
|
|
47
|
+
expect(isAppBackground(stateHost('background'))).toBe(true);
|
|
48
|
+
expect(isAppBackground(stateHost('active'))).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('isAppInactive', () => {
|
|
53
|
+
it('reports inactive only for the inactive state', () => {
|
|
54
|
+
expect(isAppInactive(stateHost('inactive'))).toBe(true);
|
|
55
|
+
expect(isAppInactive(stateHost('active'))).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|