@symbiote-native/keep-awake 0.0.1 → 0.2.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.
Files changed (40) hide show
  1. package/build/angular/index.d.ts +1 -1
  2. package/build/angular/index.js +2 -2
  3. package/build/angular/services/keep-awake.service/index.js +7 -6
  4. package/build/core/index.js +1 -1
  5. package/build/core/keep-awake.js +1 -1
  6. package/build/core/listener-attachment.d.ts +8 -0
  7. package/build/core/listener-attachment.js +18 -0
  8. package/build/react/hooks/use-keep-awake/index.js +14 -17
  9. package/build/react/index.d.ts +1 -1
  10. package/build/react/index.js +2 -2
  11. package/build/svelte/index.d.ts +2 -0
  12. package/build/svelte/index.js +5 -0
  13. package/build/svelte/runes/use-keep-awake.svelte.d.ts +2 -0
  14. package/build/svelte/runes/use-keep-awake.svelte.js +37 -0
  15. package/build/vue/composables/use-keep-awake/index.js +7 -6
  16. package/build/vue/index.d.ts +1 -1
  17. package/build/vue/index.js +2 -2
  18. package/build-ngc/angular/index.d.ts +1 -1
  19. package/build-ngc/angular/index.js +1 -1
  20. package/build-ngc/angular/index.js.map +1 -1
  21. package/build-ngc/angular/services/keep-awake.service/index.js +10 -9
  22. package/build-ngc/angular/services/keep-awake.service/index.js.map +1 -1
  23. package/build-ngc/core/listener-attachment.d.ts +8 -0
  24. package/build-ngc/core/listener-attachment.js +19 -0
  25. package/build-ngc/core/listener-attachment.js.map +1 -0
  26. package/package.json +30 -13
  27. package/src/angular/index.ts +1 -11
  28. package/src/angular/services/keep-awake.service/index.ts +8 -11
  29. package/src/core/listener-attachment.ts +43 -0
  30. package/src/react/hooks/use-keep-awake/index.ts +14 -22
  31. package/src/react/index.ts +1 -11
  32. package/src/svelte/index.ts +6 -0
  33. package/src/svelte/runes/use-keep-awake.svelte.ts +41 -0
  34. package/src/svelte/svelte-ambient.d.ts +6 -0
  35. package/src/vue/composables/use-keep-awake/index.ts +7 -11
  36. package/src/vue/index.ts +1 -11
  37. package/src/angular/services/keep-awake.service/keep-awake.service.test.ts +0 -98
  38. package/src/core/keep-awake.test.ts +0 -119
  39. package/src/react/hooks/use-keep-awake/use-keep-awake.test.tsx +0 -70
  40. package/src/vue/composables/use-keep-awake/use-keep-awake.test.ts +0 -76
@@ -1,2 +1,2 @@
1
1
  export { KeepAwakeService } from './services/keep-awake.service';
2
- export { ExpoKeepAwakeTag, isAvailableAsync, activateKeepAwakeAsync, deactivateKeepAwake, addListener, type KeepAwakeEvent, type KeepAwakeListener, type KeepAwakeOptions, type EventSubscription, } from '../core';
2
+ export * from '../core';
@@ -1,4 +1,4 @@
1
1
  // KeepAwakeService is the Angular-only lifecycle half; the free functions and event
2
2
  // subscription plumbing all live in core, shared with React/Vue.
3
- export { KeepAwakeService } from './services/keep-awake.service';
4
- export { ExpoKeepAwakeTag, isAvailableAsync, activateKeepAwakeAsync, deactivateKeepAwake, addListener, } from '../core';
3
+ export { KeepAwakeService } from './services/keep-awake.service/index.js';
4
+ export * from '../core/index.js';
@@ -33,7 +33,8 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
33
33
  return useValue ? value : void 0;
34
34
  };
35
35
  import { effect, inject, Injectable, Injector } from '@angular/core';
36
- import { activateKeepAwakeAsync, addListener, deactivateKeepAwake, } from '../../../core';
36
+ import { activateKeepAwakeAsync, deactivateKeepAwake } from '../../../core/index.js';
37
+ import { createKeepAwakeListenerAttachment } from '../../../core/listener-attachment.js';
37
38
  // Angular twin of React's `useKeepAwake` hook and Vue's `useKeepAwake` composable. See
38
39
  // LowPowerModeService (packages/battery) for the `connect()`/`effect()` pattern rationale — same
39
40
  // wiring, but keep-awake has no value to surface as a Signal since it's a pure side effect for
@@ -60,14 +61,14 @@ let KeepAwakeService = (() => {
60
61
  connect(tag, options) {
61
62
  const tagOrDefault = tag ?? `keep-awake-tag-${++tagCounter}`;
62
63
  effect(onCleanup => {
64
+ // Per effect run: onCleanup releases this exact attachment, so a re-run always starts
65
+ // from a fresh one rather than reviving a released attach.
66
+ const attachment = createKeepAwakeListenerAttachment(tagOrDefault, options?.listener);
63
67
  activateKeepAwakeAsync(tagOrDefault)
64
- .then(() => {
65
- if (options?.listener) {
66
- addListener(tagOrDefault, options.listener);
67
- }
68
- })
68
+ .then(() => attachment.attach())
69
69
  .catch(() => { });
70
70
  onCleanup(() => {
71
+ attachment.release();
71
72
  if (options?.suppressDeactivateWarnings) {
72
73
  deactivateKeepAwake(tagOrDefault).catch(() => { });
73
74
  }
@@ -1 +1 @@
1
- export { ExpoKeepAwakeTag, isAvailableAsync, activateKeepAwakeAsync, deactivateKeepAwake, addListener, } from './keep-awake';
1
+ export { ExpoKeepAwakeTag, isAvailableAsync, activateKeepAwakeAsync, deactivateKeepAwake, addListener, } from './keep-awake.js';
@@ -4,7 +4,7 @@
4
4
  // upstream keep-awake throws from `addListener` when the native module lacks
5
5
  // `addListenerForTag`, matching local-auth/haptics's UnavailabilityError convention.
6
6
  import { UnavailabilityError } from 'expo-modules-core';
7
- import { expoKeepAwake } from './native-module';
7
+ import { expoKeepAwake } from './native-module.js';
8
8
  export const ExpoKeepAwakeTag = 'ExpoKeepAwakeDefaultTag';
9
9
  /** Resolves with whether the keep-awake API is available on this device. */
10
10
  export async function isAvailableAsync() {
@@ -0,0 +1,8 @@
1
+ import type { KeepAwakeListener } from './types';
2
+ export type IKeepAwakeListenerAttachment = {
3
+ /** Registers the listener for `tag`. A no-op once `release` has run, or with no listener. */
4
+ attach: () => void;
5
+ /** Cancels a not-yet-run `attach` and removes the subscription if one was already registered. */
6
+ release: () => void;
7
+ };
8
+ export declare function createKeepAwakeListenerAttachment(tag: string, listener?: KeepAwakeListener): IKeepAwakeListenerAttachment;
@@ -0,0 +1,18 @@
1
+ import { addListener } from './keep-awake.js';
2
+ export function createKeepAwakeListenerAttachment(tag, listener) {
3
+ let isReleased = false;
4
+ let subscription;
5
+ return {
6
+ attach: () => {
7
+ if (isReleased || !listener) {
8
+ return;
9
+ }
10
+ subscription = addListener(tag, listener);
11
+ },
12
+ release: () => {
13
+ isReleased = true;
14
+ subscription?.remove();
15
+ subscription = undefined;
16
+ },
17
+ };
18
+ }
@@ -1,29 +1,26 @@
1
- // React lifecycle wiring over the framework-agnostic core (core/keep-awake.ts) — activates on
2
- // mount, deactivates on unmount, mirroring upstream's own useKeepAwake. Uses React's useId for
3
- // the default per-instance tag exactly like upstream, so two components calling useKeepAwake()
4
- // concurrently without an explicit tag don't clobber each other's activation/deactivation.
1
+ // React lifecycle wiring over the framework-agnostic core (core/keep-awake.ts): activates on
2
+ // mount, deactivates on unmount, mirroring upstream's useKeepAwake. Uses React's useId for the
3
+ // default per-instance tag so concurrent callers without an explicit tag don't clobber each
4
+ // other's activation/deactivation.
5
5
  //
6
- // Upstream also checks the native module's own `addListenerForTag` presence directly before
7
- // calling `addListener`; that private module isn't reachable from here (only core's public API
8
- // is), so registering the listener just goes through the same promise chain — an
9
- // UnavailabilityError from `addListener` is swallowed by the trailing `.catch(() => {})` exactly
10
- // like a rejected `activateKeepAwakeAsync`, same net effect as upstream's own guard.
6
+ // Upstream guards `addListener` by checking the native module's `addListenerForTag` directly;
7
+ // that's not reachable here (only core's public API is), so an UnavailabilityError from
8
+ // `addListener` is just swallowed by the trailing `.catch(() => {})` instead - same net effect.
11
9
  import { useEffect, useId } from 'react';
12
- import { activateKeepAwakeAsync, addListener, deactivateKeepAwake, } from '../../../core';
10
+ import { activateKeepAwakeAsync, deactivateKeepAwake } from '../../../core/index.js';
11
+ import { createKeepAwakeListenerAttachment } from '../../../core/listener-attachment.js';
13
12
  export function useKeepAwake(tag, options) {
14
13
  const defaultTag = useId();
15
14
  const tagOrDefault = tag ?? defaultTag;
16
15
  useEffect(() => {
17
- let isMounted = true;
16
+ // Per effect run, not per hook call - a changed tag tears the old attachment down with the
17
+ // rest of the cleanup and starts a fresh one.
18
+ const attachment = createKeepAwakeListenerAttachment(tagOrDefault, options?.listener);
18
19
  activateKeepAwakeAsync(tagOrDefault)
19
- .then(() => {
20
- if (isMounted && options?.listener) {
21
- addListener(tagOrDefault, options.listener);
22
- }
23
- })
20
+ .then(() => attachment.attach())
24
21
  .catch(() => { });
25
22
  return () => {
26
- isMounted = false;
23
+ attachment.release();
27
24
  if (options?.suppressDeactivateWarnings) {
28
25
  deactivateKeepAwake(tagOrDefault).catch(() => { });
29
26
  }
@@ -1,2 +1,2 @@
1
1
  export { useKeepAwake } from './hooks/use-keep-awake';
2
- export { ExpoKeepAwakeTag, isAvailableAsync, activateKeepAwakeAsync, deactivateKeepAwake, addListener, type KeepAwakeEvent, type KeepAwakeListener, type KeepAwakeOptions, type EventSubscription, } from '../core';
2
+ export * from '../core';
@@ -1,5 +1,5 @@
1
1
  // @symbiote-native/keep-awake/react: the React entry over the framework-agnostic core. The
2
2
  // lifecycle bucket is `hooks/` (never `composables/`, that's Vue's term) — see
3
3
  // adapters/react/src/hooks for the same convention.
4
- export { useKeepAwake } from './hooks/use-keep-awake';
5
- export { ExpoKeepAwakeTag, isAvailableAsync, activateKeepAwakeAsync, deactivateKeepAwake, addListener, } from '../core';
4
+ export { useKeepAwake } from './hooks/use-keep-awake/index.js';
5
+ export * from '../core/index.js';
@@ -0,0 +1,2 @@
1
+ export { useKeepAwake } from './runes/use-keep-awake.svelte';
2
+ export * from '../core';
@@ -0,0 +1,5 @@
1
+ // @symbiote-native/keep-awake/svelte: the Svelte entry over the framework-agnostic core. The
2
+ // lifecycle bucket is `runes/` (never `hooks/`/`composables/`, those are React's and Vue's terms)
3
+ // — see adapters/svelte/src/runes for the same convention.
4
+ export { useKeepAwake } from './runes/use-keep-awake.svelte';
5
+ export * from '../core/index.js';
@@ -0,0 +1,2 @@
1
+ import { type KeepAwakeOptions } from '../../core';
2
+ export declare function useKeepAwake(tag?: string, options?: KeepAwakeOptions): void;
@@ -0,0 +1,37 @@
1
+ // Svelte lifecycle wiring over the framework-agnostic core (core/keep-awake.ts): activates on
2
+ // mount, deactivates on unmount, mirroring React's hook and Vue's composable.
3
+ //
4
+ // `.svelte.ts` extension: `$effect` only works there outside a `.svelte` component. `runes/` is
5
+ // Svelte's name for what React calls `hooks/` and Vue calls `composables/`.
6
+ //
7
+ // The effect reads no reactive state, so it fires once per mount (its return value is the
8
+ // teardown) - same once-per-mount contract as Vue's composable, unlike React's hook, which
9
+ // re-runs when `tagOrDefault` changes.
10
+ //
11
+ // `tag`/`options` are plain values, not getters, because the `<script>` body (like Vue's setup)
12
+ // runs once - nothing here re-reads them.
13
+ //
14
+ // No `useId` in Svelte, so the default tag comes from a module-local counter, as in Vue's
15
+ // composable.
16
+ import { activateKeepAwakeAsync, deactivateKeepAwake } from '../../core/index.js';
17
+ import { createKeepAwakeListenerAttachment } from '../../core/listener-attachment.js';
18
+ let tagCounter = 0;
19
+ export function useKeepAwake(tag, options) {
20
+ const tagOrDefault = tag ?? `keep-awake-tag-${++tagCounter}`;
21
+ $effect(() => {
22
+ // Inside the effect, so the attachment shares the teardown returned below.
23
+ const attachment = createKeepAwakeListenerAttachment(tagOrDefault, options?.listener);
24
+ activateKeepAwakeAsync(tagOrDefault)
25
+ .then(() => attachment.attach())
26
+ .catch(() => { });
27
+ return () => {
28
+ attachment.release();
29
+ if (options?.suppressDeactivateWarnings) {
30
+ deactivateKeepAwake(tagOrDefault).catch(() => { });
31
+ }
32
+ else {
33
+ deactivateKeepAwake(tagOrDefault);
34
+ }
35
+ };
36
+ });
37
+ }
@@ -4,20 +4,21 @@
4
4
  // counter instead — simplest robust way to give each composable instance its own tag when the
5
5
  // caller doesn't supply one.
6
6
  import { onMounted, onUnmounted } from '@vue/runtime-core';
7
- import { activateKeepAwakeAsync, addListener, deactivateKeepAwake, } from '../../../core';
7
+ import { activateKeepAwakeAsync, deactivateKeepAwake } from '../../../core/index.js';
8
+ import { createKeepAwakeListenerAttachment } from '../../../core/listener-attachment.js';
8
9
  let tagCounter = 0;
9
10
  export function useKeepAwake(tag, options) {
10
11
  const tagOrDefault = tag ?? `keep-awake-tag-${++tagCounter}`;
12
+ // Setup body scope: it runs once per instance, so one attachment spans the single
13
+ // onMounted/onUnmounted pair - no need to re-create it on the mount hook.
14
+ const attachment = createKeepAwakeListenerAttachment(tagOrDefault, options?.listener);
11
15
  onMounted(() => {
12
16
  activateKeepAwakeAsync(tagOrDefault)
13
- .then(() => {
14
- if (options?.listener) {
15
- addListener(tagOrDefault, options.listener);
16
- }
17
- })
17
+ .then(() => attachment.attach())
18
18
  .catch(() => { });
19
19
  });
20
20
  onUnmounted(() => {
21
+ attachment.release();
21
22
  if (options?.suppressDeactivateWarnings) {
22
23
  deactivateKeepAwake(tagOrDefault).catch(() => { });
23
24
  }
@@ -1,2 +1,2 @@
1
1
  export { useKeepAwake } from './composables/use-keep-awake';
2
- export { ExpoKeepAwakeTag, isAvailableAsync, activateKeepAwakeAsync, deactivateKeepAwake, addListener, type KeepAwakeEvent, type KeepAwakeListener, type KeepAwakeOptions, type EventSubscription, } from '../core';
2
+ export * from '../core';
@@ -1,5 +1,5 @@
1
1
  // @symbiote-native/keep-awake/vue: the Vue entry over the framework-agnostic core. The
2
2
  // lifecycle bucket is `composables/` (never `hooks/`, that's React's term) — see
3
3
  // adapters/vue/src/composables for the same convention.
4
- export { useKeepAwake } from './composables/use-keep-awake';
5
- export { ExpoKeepAwakeTag, isAvailableAsync, activateKeepAwakeAsync, deactivateKeepAwake, addListener, } from '../core';
4
+ export { useKeepAwake } from './composables/use-keep-awake/index.js';
5
+ export * from '../core/index.js';
@@ -1,2 +1,2 @@
1
1
  export { KeepAwakeService } from './services/keep-awake.service';
2
- export { ExpoKeepAwakeTag, isAvailableAsync, activateKeepAwakeAsync, deactivateKeepAwake, addListener, type KeepAwakeEvent, type KeepAwakeListener, type KeepAwakeOptions, type EventSubscription, } from '../core';
2
+ export * from '../core';
@@ -1,5 +1,5 @@
1
1
  // KeepAwakeService is the Angular-only lifecycle half; the free functions and event
2
2
  // subscription plumbing all live in core, shared with React/Vue.
3
3
  export { KeepAwakeService } from './services/keep-awake.service';
4
- export { ExpoKeepAwakeTag, isAvailableAsync, activateKeepAwakeAsync, deactivateKeepAwake, addListener, } from '../core';
4
+ export * from '../core';
5
5
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/angular/index.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,iEAAiE;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,OAAO,EACL,gBAAgB,EAChB,gBAAgB,EAChB,sBAAsB,EACtB,mBAAmB,EACnB,WAAW,GAKZ,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/angular/index.ts"],"names":[],"mappings":"AAAA,oFAAoF;AACpF,iEAAiE;AACjE,OAAO,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACjE,cAAc,SAAS,CAAC"}
@@ -1,5 +1,6 @@
1
1
  import { effect, inject, Injectable, Injector } from '@angular/core';
2
- import { activateKeepAwakeAsync, addListener, deactivateKeepAwake, } from '../../../core';
2
+ import { activateKeepAwakeAsync, deactivateKeepAwake } from '../../../core';
3
+ import { createKeepAwakeListenerAttachment } from '../../../core/listener-attachment';
3
4
  import * as i0 from "@angular/core";
4
5
  // Angular twin of React's `useKeepAwake` hook and Vue's `useKeepAwake` composable. See
5
6
  // LowPowerModeService (packages/battery) for the `connect()`/`effect()` pattern rationale — same
@@ -14,14 +15,14 @@ export class KeepAwakeService {
14
15
  connect(tag, options) {
15
16
  const tagOrDefault = tag ?? `keep-awake-tag-${++tagCounter}`;
16
17
  effect(onCleanup => {
18
+ // Per effect run: onCleanup releases this exact attachment, so a re-run always starts
19
+ // from a fresh one rather than reviving a released attach.
20
+ const attachment = createKeepAwakeListenerAttachment(tagOrDefault, options?.listener);
17
21
  activateKeepAwakeAsync(tagOrDefault)
18
- .then(() => {
19
- if (options?.listener) {
20
- addListener(tagOrDefault, options.listener);
21
- }
22
- })
22
+ .then(() => attachment.attach())
23
23
  .catch(() => { });
24
24
  onCleanup(() => {
25
+ attachment.release();
25
26
  if (options?.suppressDeactivateWarnings) {
26
27
  deactivateKeepAwake(tagOrDefault).catch(() => { });
27
28
  }
@@ -31,10 +32,10 @@ export class KeepAwakeService {
31
32
  });
32
33
  }, { injector: this.injector });
33
34
  }
34
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KeepAwakeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
35
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KeepAwakeService, providedIn: 'root' });
35
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: KeepAwakeService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
36
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: KeepAwakeService, providedIn: 'root' });
36
37
  }
37
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: KeepAwakeService, decorators: [{
38
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.8", ngImport: i0, type: KeepAwakeService, decorators: [{
38
39
  type: Injectable,
39
40
  args: [{ providedIn: 'root' }]
40
41
  }] });
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/angular/services/keep-awake.service/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EACL,sBAAsB,EACtB,WAAW,EACX,mBAAmB,GAEpB,MAAM,eAAe,CAAC;;AAEvB,uFAAuF;AACvF,iGAAiG;AACjG,+FAA+F;AAC/F,6FAA6F;AAC7F,yFAAyF;AACzF,EAAE;AACF,0DAA0D;AAC1D,IAAI,UAAU,GAAG,CAAC,CAAC;AAGnB,MAAM,OAAO,gBAAgB;IACV,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IAE7C,OAAO,CAAC,GAAY,EAAE,OAA0B;QAC9C,MAAM,YAAY,GAAG,GAAG,IAAI,kBAAkB,EAAE,UAAU,EAAE,CAAC;QAE7D,MAAM,CACJ,SAAS,CAAC,EAAE;YACV,sBAAsB,CAAC,YAAY,CAAC;iBACjC,IAAI,CAAC,GAAG,EAAE;gBACT,IAAI,OAAO,EAAE,QAAQ,EAAE,CAAC;oBACtB,WAAW,CAAC,YAAY,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC;gBAC9C,CAAC;YACH,CAAC,CAAC;iBACD,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YAEnB,SAAS,CAAC,GAAG,EAAE;gBACb,IAAI,OAAO,EAAE,0BAA0B,EAAE,CAAC;oBACxC,mBAAmB,CAAC,YAAY,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;gBACpD,CAAC;qBAAM,CAAC;oBACN,mBAAmB,CAAC,YAAY,CAAC,CAAC;gBACpC,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC,EACD,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAC5B,CAAC;IACJ,CAAC;uGA1BU,gBAAgB;2GAAhB,gBAAgB,cADH,MAAM;;2FACnB,gBAAgB;kBAD5B,UAAU;mBAAC,EAAE,UAAU,EAAE,MAAM,EAAE"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/angular/services/keep-awake.service/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EAAE,sBAAsB,EAAE,mBAAmB,EAAyB,MAAM,eAAe,CAAC;AACnG,OAAO,EAAE,iCAAiC,EAAE,MAAM,mCAAmC,CAAC;;AAEtF,uFAAuF;AACvF,iGAAiG;AACjG,+FAA+F;AAC/F,6FAA6F;AAC7F,yFAAyF;AACzF,EAAE;AACF,0DAA0D;AAC1D,IAAI,UAAU,GAAG,CAAC,CAAC;AAGnB,MAAM,OAAO,gBAAgB;IACV,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC;IAE7C,OAAO,CAAC,GAAY,EAAE,OAA0B;QAC9C,MAAM,YAAY,GAAG,GAAG,IAAI,kBAAkB,EAAE,UAAU,EAAE,CAAC;QAE7D,MAAM,CACJ,SAAS,CAAC,EAAE;YACV,sFAAsF;YACtF,2DAA2D;YAC3D,MAAM,UAAU,GAAG,iCAAiC,CAAC,YAAY,EAAE,OAAO,EAAE,QAAQ,CAAC,CAAC;YAEtF,sBAAsB,CAAC,YAAY,CAAC;iBACjC,IAAI,CAAC,GAAG,EAAE,CAAC,UAAU,CAAC,MAAM,EAAE,CAAC;iBAC/B,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YAEnB,SAAS,CAAC,GAAG,EAAE;gBACb,UAAU,CAAC,OAAO,EAAE,CAAC;gBACrB,IAAI,OAAO,EAAE,0BAA0B,EAAE,CAAC;oBACxC,mBAAmB,CAAC,YAAY,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;gBACpD,CAAC;qBAAM,CAAC;oBACN,mBAAmB,CAAC,YAAY,CAAC,CAAC;gBACpC,CAAC;YACH,CAAC,CAAC,CAAC;QACL,CAAC,EACD,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,CAC5B,CAAC;IACJ,CAAC;uGA3BU,gBAAgB;2GAAhB,gBAAgB,cADH,MAAM;;2FACnB,gBAAgB;kBAD5B,UAAU;mBAAC,EAAE,UAAU,EAAE,MAAM,EAAE"}
@@ -0,0 +1,8 @@
1
+ import type { KeepAwakeListener } from './types';
2
+ export type IKeepAwakeListenerAttachment = {
3
+ /** Registers the listener for `tag`. A no-op once `release` has run, or with no listener. */
4
+ attach: () => void;
5
+ /** Cancels a not-yet-run `attach` and removes the subscription if one was already registered. */
6
+ release: () => void;
7
+ };
8
+ export declare function createKeepAwakeListenerAttachment(tag: string, listener?: KeepAwakeListener): IKeepAwakeListenerAttachment;
@@ -0,0 +1,19 @@
1
+ import { addListener } from './keep-awake';
2
+ export function createKeepAwakeListenerAttachment(tag, listener) {
3
+ let isReleased = false;
4
+ let subscription;
5
+ return {
6
+ attach: () => {
7
+ if (isReleased || !listener) {
8
+ return;
9
+ }
10
+ subscription = addListener(tag, listener);
11
+ },
12
+ release: () => {
13
+ isReleased = true;
14
+ subscription?.remove();
15
+ subscription = undefined;
16
+ },
17
+ };
18
+ }
19
+ //# sourceMappingURL=listener-attachment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"listener-attachment.js","sourceRoot":"","sources":["../../src/core/listener-attachment.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAU3C,MAAM,UAAU,iCAAiC,CAC/C,GAAW,EACX,QAA4B;IAE5B,IAAI,UAAU,GAAG,KAAK,CAAC;IACvB,IAAI,YAA2C,CAAC;IAEhD,OAAO;QACL,MAAM,EAAE,GAAG,EAAE;YACX,IAAI,UAAU,IAAI,CAAC,QAAQ,EAAE,CAAC;gBAC5B,OAAO;YACT,CAAC;YACD,YAAY,GAAG,WAAW,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAC5C,CAAC;QACD,OAAO,EAAE,GAAG,EAAE;YACZ,UAAU,GAAG,IAAI,CAAC;YAClB,YAAY,EAAE,MAAM,EAAE,CAAC;YACvB,YAAY,GAAG,SAAS,CAAC;QAC3B,CAAC;KACF,CAAC;AACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@symbiote-native/keep-awake",
3
- "version": "0.0.1",
3
+ "version": "0.2.0",
4
4
  "description": "expo-keep-awake wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, and Angular adapters. Keeps the screen awake for the lifetime of a mounted component/composable/service, with an optional listener for keep-awake state changes.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -30,6 +30,10 @@
30
30
  "types": "./build/react/index.d.ts",
31
31
  "default": "./build/react/index.js"
32
32
  },
33
+ "./svelte": {
34
+ "types": "./build/svelte/index.d.ts",
35
+ "default": "./build/svelte/index.js"
36
+ },
33
37
  "./angular": {
34
38
  "types": "./build-ngc/angular/index.d.ts",
35
39
  "react-native": "./build-ngc/angular/index.js",
@@ -40,7 +44,10 @@
40
44
  "src",
41
45
  "build",
42
46
  "build-ngc",
43
- "native-link.json"
47
+ "native-link.json",
48
+ "!src/**/*.test.*",
49
+ "!src/**/*.spec.*",
50
+ "!src/**/*.detox.*"
44
51
  ],
45
52
  "publishConfig": {
46
53
  "access": "public"
@@ -50,15 +57,17 @@
50
57
  "expo-modules-core": "57.0.5"
51
58
  },
52
59
  "peerDependencies": {
60
+ "@symbiote-native/angular": ">=0.6.2",
53
61
  "@symbiote-native/engine": ">=0.1.7",
62
+ "@symbiote-native/react": ">=0.2.8",
63
+ "@symbiote-native/svelte": ">=0.1.0",
64
+ "@symbiote-native/vue": ">=0.3.8",
54
65
  "@angular/core": ">=20",
55
66
  "@vue/runtime-core": "^3.5.13",
56
67
  "react": ">=19.0.0",
57
68
  "react-native": ">=0.86",
58
- "vue": ">=3.5.0",
59
- "@symbiote-native/angular": "0.6.1",
60
- "@symbiote-native/react": "0.2.8",
61
- "@symbiote-native/vue": "0.3.8"
69
+ "svelte": ">=5.56.0",
70
+ "vue": ">=3.5.0"
62
71
  },
63
72
  "peerDependenciesMeta": {
64
73
  "@symbiote-native/angular": {
@@ -67,6 +76,9 @@
67
76
  "@symbiote-native/react": {
68
77
  "optional": true
69
78
  },
79
+ "@symbiote-native/svelte": {
80
+ "optional": true
81
+ },
70
82
  "@symbiote-native/vue": {
71
83
  "optional": true
72
84
  },
@@ -79,24 +91,29 @@
79
91
  "react": {
80
92
  "optional": true
81
93
  },
94
+ "svelte": {
95
+ "optional": true
96
+ },
82
97
  "vue": {
83
98
  "optional": true
84
99
  }
85
100
  },
86
101
  "devDependencies": {
87
- "@angular/compiler": "^22",
88
- "@angular/compiler-cli": "^22",
89
- "@angular/core": "^22",
102
+ "@angular/compiler": "~22.0.8",
103
+ "@angular/compiler-cli": "~22.0.8",
104
+ "@angular/core": "~22.0.8",
90
105
  "@types/node": "^26.0.0",
91
106
  "@types/react": "^19.2.0",
92
107
  "@vue/runtime-core": "^3.5.13",
93
108
  "react": "19.2.3",
109
+ "svelte": "^5.56.0",
94
110
  "typescript": "~6.0.0",
111
+ "@symbiote-native/angular": "0.7.0",
112
+ "@symbiote-native/engine": "0.2.0",
113
+ "@symbiote-native/react": "0.3.0",
114
+ "@symbiote-native/svelte": "0.2.0",
95
115
  "@symbiote-native/test-utils": "0.1.6",
96
- "@symbiote-native/engine": "0.1.7",
97
- "@symbiote-native/react": "0.2.8",
98
- "@symbiote-native/angular": "0.6.1",
99
- "@symbiote-native/vue": "0.3.8"
116
+ "@symbiote-native/vue": "0.4.0"
100
117
  },
101
118
  "scripts": {
102
119
  "typecheck": "tsc --build",
@@ -1,14 +1,4 @@
1
1
  // KeepAwakeService is the Angular-only lifecycle half; the free functions and event
2
2
  // subscription plumbing all live in core, shared with React/Vue.
3
3
  export { KeepAwakeService } from './services/keep-awake.service';
4
- export {
5
- ExpoKeepAwakeTag,
6
- isAvailableAsync,
7
- activateKeepAwakeAsync,
8
- deactivateKeepAwake,
9
- addListener,
10
- type KeepAwakeEvent,
11
- type KeepAwakeListener,
12
- type KeepAwakeOptions,
13
- type EventSubscription,
14
- } from '../core';
4
+ export * from '../core';
@@ -1,10 +1,6 @@
1
1
  import { effect, inject, Injectable, Injector } from '@angular/core';
2
- import {
3
- activateKeepAwakeAsync,
4
- addListener,
5
- deactivateKeepAwake,
6
- type KeepAwakeOptions,
7
- } from '../../../core';
2
+ import { activateKeepAwakeAsync, deactivateKeepAwake, type KeepAwakeOptions } from '../../../core';
3
+ import { createKeepAwakeListenerAttachment } from '../../../core/listener-attachment';
8
4
 
9
5
  // Angular twin of React's `useKeepAwake` hook and Vue's `useKeepAwake` composable. See
10
6
  // LowPowerModeService (packages/battery) for the `connect()`/`effect()` pattern rationale — same
@@ -24,15 +20,16 @@ export class KeepAwakeService {
24
20
 
25
21
  effect(
26
22
  onCleanup => {
23
+ // Per effect run: onCleanup releases this exact attachment, so a re-run always starts
24
+ // from a fresh one rather than reviving a released attach.
25
+ const attachment = createKeepAwakeListenerAttachment(tagOrDefault, options?.listener);
26
+
27
27
  activateKeepAwakeAsync(tagOrDefault)
28
- .then(() => {
29
- if (options?.listener) {
30
- addListener(tagOrDefault, options.listener);
31
- }
32
- })
28
+ .then(() => attachment.attach())
33
29
  .catch(() => {});
34
30
 
35
31
  onCleanup(() => {
32
+ attachment.release();
36
33
  if (options?.suppressDeactivateWarnings) {
37
34
  deactivateKeepAwake(tagOrDefault).catch(() => {});
38
35
  } else {
@@ -0,0 +1,43 @@
1
+ // The listener half of a keep-awake session, plus the teardown race that comes with it:
2
+ // activation is async, so the consumer can be gone by the time it resolves. Whoever tears down
3
+ // first has to do two things - cancel an attach that hasn't run yet, AND remove a subscription
4
+ // that already landed. Miss either and the listener outlives the consumer with nothing left
5
+ // holding a reference to remove it.
6
+ //
7
+ // It lives here rather than in each adapter because the flag-plus-subscription bookkeeping is
8
+ // identical in all four; only WHERE attach/release get called differs (useEffect cleanup,
9
+ // onUnmounted, effect's onCleanup, $effect teardown). Deliberately NOT re-exported from
10
+ // core/index.ts - every framework entry does `export * from '../core'`, and this is lifecycle
11
+ // plumbing for the adapters, not public API.
12
+ import type { EventSubscription } from 'expo-modules-core';
13
+ import { addListener } from './keep-awake';
14
+ import type { KeepAwakeListener } from './types';
15
+
16
+ export type IKeepAwakeListenerAttachment = {
17
+ /** Registers the listener for `tag`. A no-op once `release` has run, or with no listener. */
18
+ attach: () => void;
19
+ /** Cancels a not-yet-run `attach` and removes the subscription if one was already registered. */
20
+ release: () => void;
21
+ };
22
+
23
+ export function createKeepAwakeListenerAttachment(
24
+ tag: string,
25
+ listener?: KeepAwakeListener,
26
+ ): IKeepAwakeListenerAttachment {
27
+ let isReleased = false;
28
+ let subscription: EventSubscription | undefined;
29
+
30
+ return {
31
+ attach: () => {
32
+ if (isReleased || !listener) {
33
+ return;
34
+ }
35
+ subscription = addListener(tag, listener);
36
+ },
37
+ release: () => {
38
+ isReleased = true;
39
+ subscription?.remove();
40
+ subscription = undefined;
41
+ },
42
+ };
43
+ }
@@ -1,38 +1,30 @@
1
- // React lifecycle wiring over the framework-agnostic core (core/keep-awake.ts) — activates on
2
- // mount, deactivates on unmount, mirroring upstream's own useKeepAwake. Uses React's useId for
3
- // the default per-instance tag exactly like upstream, so two components calling useKeepAwake()
4
- // concurrently without an explicit tag don't clobber each other's activation/deactivation.
1
+ // React lifecycle wiring over the framework-agnostic core (core/keep-awake.ts): activates on
2
+ // mount, deactivates on unmount, mirroring upstream's useKeepAwake. Uses React's useId for the
3
+ // default per-instance tag so concurrent callers without an explicit tag don't clobber each
4
+ // other's activation/deactivation.
5
5
  //
6
- // Upstream also checks the native module's own `addListenerForTag` presence directly before
7
- // calling `addListener`; that private module isn't reachable from here (only core's public API
8
- // is), so registering the listener just goes through the same promise chain — an
9
- // UnavailabilityError from `addListener` is swallowed by the trailing `.catch(() => {})` exactly
10
- // like a rejected `activateKeepAwakeAsync`, same net effect as upstream's own guard.
6
+ // Upstream guards `addListener` by checking the native module's `addListenerForTag` directly;
7
+ // that's not reachable here (only core's public API is), so an UnavailabilityError from
8
+ // `addListener` is just swallowed by the trailing `.catch(() => {})` instead - same net effect.
11
9
  import { useEffect, useId } from 'react';
12
- import {
13
- activateKeepAwakeAsync,
14
- addListener,
15
- deactivateKeepAwake,
16
- type KeepAwakeOptions,
17
- } from '../../../core';
10
+ import { activateKeepAwakeAsync, deactivateKeepAwake, type KeepAwakeOptions } from '../../../core';
11
+ import { createKeepAwakeListenerAttachment } from '../../../core/listener-attachment';
18
12
 
19
13
  export function useKeepAwake(tag?: string, options?: KeepAwakeOptions): void {
20
14
  const defaultTag = useId();
21
15
  const tagOrDefault = tag ?? defaultTag;
22
16
 
23
17
  useEffect(() => {
24
- let isMounted = true;
18
+ // Per effect run, not per hook call - a changed tag tears the old attachment down with the
19
+ // rest of the cleanup and starts a fresh one.
20
+ const attachment = createKeepAwakeListenerAttachment(tagOrDefault, options?.listener);
25
21
 
26
22
  activateKeepAwakeAsync(tagOrDefault)
27
- .then(() => {
28
- if (isMounted && options?.listener) {
29
- addListener(tagOrDefault, options.listener);
30
- }
31
- })
23
+ .then(() => attachment.attach())
32
24
  .catch(() => {});
33
25
 
34
26
  return () => {
35
- isMounted = false;
27
+ attachment.release();
36
28
  if (options?.suppressDeactivateWarnings) {
37
29
  deactivateKeepAwake(tagOrDefault).catch(() => {});
38
30
  } else {
@@ -3,14 +3,4 @@
3
3
  // adapters/react/src/hooks for the same convention.
4
4
 
5
5
  export { useKeepAwake } from './hooks/use-keep-awake';
6
- export {
7
- ExpoKeepAwakeTag,
8
- isAvailableAsync,
9
- activateKeepAwakeAsync,
10
- deactivateKeepAwake,
11
- addListener,
12
- type KeepAwakeEvent,
13
- type KeepAwakeListener,
14
- type KeepAwakeOptions,
15
- type EventSubscription,
16
- } from '../core';
6
+ export * from '../core';
@@ -0,0 +1,6 @@
1
+ // @symbiote-native/keep-awake/svelte: the Svelte entry over the framework-agnostic core. The
2
+ // lifecycle bucket is `runes/` (never `hooks/`/`composables/`, those are React's and Vue's terms)
3
+ // — see adapters/svelte/src/runes for the same convention.
4
+
5
+ export { useKeepAwake } from './runes/use-keep-awake.svelte';
6
+ export * from '../core';
@@ -0,0 +1,41 @@
1
+ // Svelte lifecycle wiring over the framework-agnostic core (core/keep-awake.ts): activates on
2
+ // mount, deactivates on unmount, mirroring React's hook and Vue's composable.
3
+ //
4
+ // `.svelte.ts` extension: `$effect` only works there outside a `.svelte` component. `runes/` is
5
+ // Svelte's name for what React calls `hooks/` and Vue calls `composables/`.
6
+ //
7
+ // The effect reads no reactive state, so it fires once per mount (its return value is the
8
+ // teardown) - same once-per-mount contract as Vue's composable, unlike React's hook, which
9
+ // re-runs when `tagOrDefault` changes.
10
+ //
11
+ // `tag`/`options` are plain values, not getters, because the `<script>` body (like Vue's setup)
12
+ // runs once - nothing here re-reads them.
13
+ //
14
+ // No `useId` in Svelte, so the default tag comes from a module-local counter, as in Vue's
15
+ // composable.
16
+ import { activateKeepAwakeAsync, deactivateKeepAwake, type KeepAwakeOptions } from '../../core';
17
+ import { createKeepAwakeListenerAttachment } from '../../core/listener-attachment';
18
+
19
+ let tagCounter = 0;
20
+
21
+ export function useKeepAwake(tag?: string, options?: KeepAwakeOptions): void {
22
+ const tagOrDefault = tag ?? `keep-awake-tag-${++tagCounter}`;
23
+
24
+ $effect(() => {
25
+ // Inside the effect, so the attachment shares the teardown returned below.
26
+ const attachment = createKeepAwakeListenerAttachment(tagOrDefault, options?.listener);
27
+
28
+ activateKeepAwakeAsync(tagOrDefault)
29
+ .then(() => attachment.attach())
30
+ .catch(() => {});
31
+
32
+ return () => {
33
+ attachment.release();
34
+ if (options?.suppressDeactivateWarnings) {
35
+ deactivateKeepAwake(tagOrDefault).catch(() => {});
36
+ } else {
37
+ deactivateKeepAwake(tagOrDefault);
38
+ }
39
+ };
40
+ });
41
+ }
@@ -0,0 +1,6 @@
1
+ // Pulls in svelte's ambient global declarations ($state/$effect/...) for this package's tsc
2
+ // program. adapters/svelte gets these for free because render.ts does a real value import from
3
+ // 'svelte' (any file in a program importing a module loads its ambient globals for the whole
4
+ // program); this package has no such import, so a triple-slash reference does the same job
5
+ // explicitly rather than relying on an incidental side-effect.
6
+ /// <reference types="svelte" />
@@ -4,29 +4,25 @@
4
4
  // counter instead — simplest robust way to give each composable instance its own tag when the
5
5
  // caller doesn't supply one.
6
6
  import { onMounted, onUnmounted } from '@vue/runtime-core';
7
- import {
8
- activateKeepAwakeAsync,
9
- addListener,
10
- deactivateKeepAwake,
11
- type KeepAwakeOptions,
12
- } from '../../../core';
7
+ import { activateKeepAwakeAsync, deactivateKeepAwake, type KeepAwakeOptions } from '../../../core';
8
+ import { createKeepAwakeListenerAttachment } from '../../../core/listener-attachment';
13
9
 
14
10
  let tagCounter = 0;
15
11
 
16
12
  export function useKeepAwake(tag?: string, options?: KeepAwakeOptions): void {
17
13
  const tagOrDefault = tag ?? `keep-awake-tag-${++tagCounter}`;
14
+ // Setup body scope: it runs once per instance, so one attachment spans the single
15
+ // onMounted/onUnmounted pair - no need to re-create it on the mount hook.
16
+ const attachment = createKeepAwakeListenerAttachment(tagOrDefault, options?.listener);
18
17
 
19
18
  onMounted(() => {
20
19
  activateKeepAwakeAsync(tagOrDefault)
21
- .then(() => {
22
- if (options?.listener) {
23
- addListener(tagOrDefault, options.listener);
24
- }
25
- })
20
+ .then(() => attachment.attach())
26
21
  .catch(() => {});
27
22
  });
28
23
 
29
24
  onUnmounted(() => {
25
+ attachment.release();
30
26
  if (options?.suppressDeactivateWarnings) {
31
27
  deactivateKeepAwake(tagOrDefault).catch(() => {});
32
28
  } else {
package/src/vue/index.ts CHANGED
@@ -3,14 +3,4 @@
3
3
  // adapters/vue/src/composables for the same convention.
4
4
 
5
5
  export { useKeepAwake } from './composables/use-keep-awake';
6
- export {
7
- ExpoKeepAwakeTag,
8
- isAvailableAsync,
9
- activateKeepAwakeAsync,
10
- deactivateKeepAwake,
11
- addListener,
12
- type KeepAwakeEvent,
13
- type KeepAwakeListener,
14
- type KeepAwakeOptions,
15
- type EventSubscription,
16
- } from '../core';
6
+ export * from '../core';
@@ -1,98 +0,0 @@
1
- // Co-located Angular-driven test (ADR 0025) for KeepAwakeService. See battery's
2
- // low-power-mode.service.test.ts for the shared rationale.
3
- //
4
- // Two host classes rather than one @Input-driven host: Angular sets @Input-bound properties
5
- // AFTER construction, so a constructor-time `connect(this.tag)` read off an @Input would always
6
- // see `undefined` — the same field-initializer/constructor timing every other service host in
7
- // this repo relies on (see battery's own PermissionsHost/BatteryLevelHost).
8
-
9
- import '@angular/compiler';
10
- import { Component, inject } from '@angular/core';
11
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
12
- import { mount, unmount } from '@symbiote-native/angular';
13
- import { installFabric } from '@symbiote-native/test-utils';
14
- import { KeepAwakeService } from './index';
15
-
16
- const activateKeepAwakeAsyncMock = vi.fn(async (_tag: string) => undefined);
17
- const addListenerMock = vi.fn();
18
- const deactivateKeepAwakeMock = vi.fn(async (_tag: string) => undefined);
19
-
20
- vi.mock('../../../core', () => ({
21
- activateKeepAwakeAsync: (tag: string) => activateKeepAwakeAsyncMock(tag),
22
- addListener: (...args: unknown[]) => addListenerMock(...args),
23
- deactivateKeepAwake: (tag: string) => deactivateKeepAwakeMock(tag),
24
- }));
25
-
26
- const ROOT_TAG = 974;
27
- const fabric = installFabric();
28
- const tick = (): Promise<void> => new Promise(resolve => setTimeout(resolve, 0));
29
-
30
- @Component({
31
- selector: 'symbiote-keep-awake-default-host',
32
- standalone: true,
33
- template: '',
34
- })
35
- class KeepAwakeDefaultHost {
36
- constructor() {
37
- inject(KeepAwakeService).connect();
38
- }
39
- }
40
-
41
- @Component({
42
- selector: 'symbiote-keep-awake-custom-host',
43
- standalone: true,
44
- template: '',
45
- })
46
- class KeepAwakeCustomHost {
47
- constructor() {
48
- inject(KeepAwakeService).connect('custom-tag');
49
- }
50
- }
51
-
52
- beforeEach(() => {
53
- activateKeepAwakeAsyncMock.mockClear();
54
- addListenerMock.mockClear();
55
- deactivateKeepAwakeMock.mockClear();
56
- activateKeepAwakeAsyncMock.mockResolvedValue(undefined);
57
- deactivateKeepAwakeMock.mockResolvedValue(undefined);
58
- });
59
-
60
- afterEach(() => {
61
- unmount(ROOT_TAG);
62
- fabric.reset();
63
- vi.clearAllMocks();
64
- });
65
-
66
- describe('KeepAwakeService.connect', () => {
67
- it('activates a default tag on mount', async () => {
68
- mount(ROOT_TAG, KeepAwakeDefaultHost);
69
- await tick();
70
-
71
- expect(activateKeepAwakeAsyncMock).toHaveBeenCalledTimes(1);
72
- expect(typeof activateKeepAwakeAsyncMock.mock.calls[0][0]).toBe('string');
73
- });
74
-
75
- it('activates the explicit tag when one is given', async () => {
76
- mount(ROOT_TAG, KeepAwakeCustomHost);
77
- await tick();
78
-
79
- expect(activateKeepAwakeAsyncMock).toHaveBeenCalledWith('custom-tag');
80
- });
81
-
82
- it('deactivates the same tag when the host component is unmounted', async () => {
83
- mount(ROOT_TAG, KeepAwakeCustomHost);
84
- await tick();
85
-
86
- unmount(ROOT_TAG);
87
- await tick();
88
-
89
- expect(deactivateKeepAwakeMock).toHaveBeenCalledWith('custom-tag');
90
- });
91
-
92
- it('never touches addListener when no options are given', async () => {
93
- mount(ROOT_TAG, KeepAwakeCustomHost);
94
- await tick();
95
-
96
- expect(addListenerMock).not.toHaveBeenCalled();
97
- });
98
- });
@@ -1,119 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from 'vitest';
2
-
3
- const FAKE_NATIVE_KEEP_AWAKE = {
4
- isAvailableAsync: vi.fn(async () => true),
5
- activate: vi.fn(async () => undefined),
6
- deactivate: vi.fn(async () => undefined),
7
- addListenerForTag: vi.fn(() => ({ remove: vi.fn() })),
8
- };
9
-
10
- // The real ExpoKeepAwake native module only exists on device — resolving it via
11
- // requireNativeModule() at import time would throw in this headless test run, so the
12
- // module-lookup file is faked in place of expo-modules-core's runtime resolution, same pattern
13
- // packages/battery/src/core/battery.test.ts uses.
14
- vi.mock('./native-module', () => ({
15
- expoKeepAwake: FAKE_NATIVE_KEEP_AWAKE,
16
- }));
17
-
18
- // expo-modules-core's real entry transitively imports 'react-native' for Platform/
19
- // TurboModuleRegistry, whose Flow-typed source Vitest's Oxc transform can't parse — same fake
20
- // packages/haptics/src/core/haptics.test.ts uses. keep-awake.ts imports UnavailabilityError as a
21
- // real value (not just a type), so it needs mocking here too.
22
- vi.mock('expo-modules-core', () => ({
23
- UnavailabilityError: class UnavailabilityError extends Error {
24
- constructor(moduleName: string, propertyName: string) {
25
- super(`${propertyName} is not available on ${moduleName}`);
26
- }
27
- },
28
- }));
29
-
30
- const {
31
- ExpoKeepAwakeTag,
32
- isAvailableAsync,
33
- activateKeepAwakeAsync,
34
- deactivateKeepAwake,
35
- addListener,
36
- } = await import('./keep-awake');
37
-
38
- afterEach(() => {
39
- vi.clearAllMocks();
40
- });
41
-
42
- describe('isAvailableAsync', () => {
43
- it('delegates to the native module when isAvailableAsync exists', async () => {
44
- await expect(isAvailableAsync()).resolves.toBe(true);
45
- });
46
-
47
- it('defaults to true when the native method is absent', async () => {
48
- const { isAvailableAsync: native } = FAKE_NATIVE_KEEP_AWAKE;
49
- // @ts-expect-error -- simulating a platform where the native module has no such method
50
- FAKE_NATIVE_KEEP_AWAKE.isAvailableAsync = undefined;
51
-
52
- await expect(isAvailableAsync()).resolves.toBe(true);
53
-
54
- FAKE_NATIVE_KEEP_AWAKE.isAvailableAsync = native;
55
- });
56
- });
57
-
58
- describe('activateKeepAwakeAsync', () => {
59
- it('activates the default tag when none is given', async () => {
60
- await activateKeepAwakeAsync();
61
-
62
- expect(FAKE_NATIVE_KEEP_AWAKE.activate).toHaveBeenCalledWith(ExpoKeepAwakeTag);
63
- });
64
-
65
- it('activates an explicit tag', async () => {
66
- await activateKeepAwakeAsync('custom-tag');
67
-
68
- expect(FAKE_NATIVE_KEEP_AWAKE.activate).toHaveBeenCalledWith('custom-tag');
69
- });
70
- });
71
-
72
- describe('deactivateKeepAwake', () => {
73
- it('deactivates the default tag when none is given', async () => {
74
- await deactivateKeepAwake();
75
-
76
- expect(FAKE_NATIVE_KEEP_AWAKE.deactivate).toHaveBeenCalledWith(ExpoKeepAwakeTag);
77
- });
78
-
79
- it('deactivates an explicit tag', async () => {
80
- await deactivateKeepAwake('custom-tag');
81
-
82
- expect(FAKE_NATIVE_KEEP_AWAKE.deactivate).toHaveBeenCalledWith('custom-tag');
83
- });
84
- });
85
-
86
- describe('addListener', () => {
87
- it('subscribes the default tag when called with a bare listener', () => {
88
- const listener = vi.fn();
89
- addListener(listener);
90
-
91
- expect(FAKE_NATIVE_KEEP_AWAKE.addListenerForTag).toHaveBeenCalledWith(
92
- ExpoKeepAwakeTag,
93
- listener,
94
- );
95
- });
96
-
97
- it('subscribes an explicit tag when given tag + listener', () => {
98
- const listener = vi.fn();
99
- addListener('custom-tag', listener);
100
-
101
- expect(FAKE_NATIVE_KEEP_AWAKE.addListenerForTag).toHaveBeenCalledWith('custom-tag', listener);
102
- });
103
-
104
- it('throws an UnavailabilityError-shaped error when addListenerForTag is absent', () => {
105
- const { addListenerForTag: native } = FAKE_NATIVE_KEEP_AWAKE;
106
- // @ts-expect-error -- simulating a platform where the native module has no such method
107
- FAKE_NATIVE_KEEP_AWAKE.addListenerForTag = undefined;
108
-
109
- expect(() => addListener(vi.fn())).toThrow(
110
- 'addListenerForTag is not available on ExpoKeepAwake',
111
- );
112
-
113
- FAKE_NATIVE_KEEP_AWAKE.addListenerForTag = native;
114
- });
115
-
116
- it('throws when no listener can be resolved', () => {
117
- expect(() => addListener('custom-tag')).toThrow('addListener requires a listener function');
118
- });
119
- });
@@ -1,70 +0,0 @@
1
- // Co-located React-driven test (ADR 0025) for useKeepAwake. See battery's
2
- // use-low-power-mode.test.tsx for the shared rationale (mocks `core`, not expo-modules-core
3
- // internals).
4
-
5
- import { createElement, type ReactElement } from 'react';
6
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
- import { mount, unmount, View } from '@symbiote-native/react';
8
- import { installFabric } from '@symbiote-native/test-utils';
9
- import { useKeepAwake } from './index';
10
-
11
- const { activateKeepAwakeAsync, addListener, deactivateKeepAwake } = vi.hoisted(() => ({
12
- activateKeepAwakeAsync: vi.fn(async (_tag: string) => undefined),
13
- addListener: vi.fn(),
14
- deactivateKeepAwake: vi.fn(async (_tag: string) => undefined),
15
- }));
16
-
17
- vi.mock('../../../core', () => ({
18
- activateKeepAwakeAsync,
19
- addListener,
20
- deactivateKeepAwake,
21
- }));
22
-
23
- const ROOT_TAG = 954;
24
-
25
- function Probe({ tag }: { tag?: string }): ReactElement {
26
- useKeepAwake(tag);
27
- return createElement(View);
28
- }
29
-
30
- const fabric = installFabric();
31
-
32
- beforeEach(() => {
33
- fabric.reset();
34
- vi.clearAllMocks();
35
- activateKeepAwakeAsync.mockResolvedValue(undefined);
36
- deactivateKeepAwake.mockResolvedValue(undefined);
37
- });
38
-
39
- afterEach(() => unmount(ROOT_TAG));
40
-
41
- describe('useKeepAwake', () => {
42
- it('activates a default tag on mount', async () => {
43
- mount(ROOT_TAG, createElement(Probe));
44
-
45
- await vi.waitFor(() => expect(activateKeepAwakeAsync).toHaveBeenCalledTimes(1));
46
- expect(typeof activateKeepAwakeAsync.mock.calls[0][0]).toBe('string');
47
- });
48
-
49
- it('activates the explicit tag when one is given', async () => {
50
- mount(ROOT_TAG, createElement(Probe, { tag: 'custom-tag' }));
51
-
52
- await vi.waitFor(() => expect(activateKeepAwakeAsync).toHaveBeenCalledWith('custom-tag'));
53
- });
54
-
55
- it('deactivates the same tag on unmount', async () => {
56
- mount(ROOT_TAG, createElement(Probe, { tag: 'custom-tag' }));
57
- await vi.waitFor(() => expect(activateKeepAwakeAsync).toHaveBeenCalledTimes(1));
58
-
59
- unmount(ROOT_TAG);
60
-
61
- expect(deactivateKeepAwake).toHaveBeenCalledWith('custom-tag');
62
- });
63
-
64
- it('never touches addListener when no options are given', async () => {
65
- mount(ROOT_TAG, createElement(Probe, { tag: 'custom-tag' }));
66
-
67
- await vi.waitFor(() => expect(activateKeepAwakeAsync).toHaveBeenCalledTimes(1));
68
- expect(addListener).not.toHaveBeenCalled();
69
- });
70
- });
@@ -1,76 +0,0 @@
1
- // Co-located Vue-driven test (ADR 0025) for useKeepAwake. See battery's
2
- // use-low-power-mode.test.ts for the shared rationale.
3
-
4
- import { defineComponent, h } from '@vue/runtime-core';
5
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
- import { mount, unmount } from '@symbiote-native/vue';
7
- import { installFabric } from '@symbiote-native/test-utils';
8
- import { useKeepAwake } from './index';
9
-
10
- const ROOT_TAG = 9954;
11
-
12
- const activateKeepAwakeAsyncMock = vi.fn(async (_tag: string) => undefined);
13
- const addListenerMock = vi.fn();
14
- const deactivateKeepAwakeMock = vi.fn(async (_tag: string) => undefined);
15
-
16
- vi.mock('../../../core', () => ({
17
- activateKeepAwakeAsync: (tag: string) => activateKeepAwakeAsyncMock(tag),
18
- addListener: (...args: unknown[]) => addListenerMock(...args),
19
- deactivateKeepAwake: (tag: string) => deactivateKeepAwakeMock(tag),
20
- }));
21
-
22
- const fabric = installFabric();
23
-
24
- beforeEach(() => {
25
- fabric.reset();
26
- activateKeepAwakeAsyncMock.mockClear();
27
- addListenerMock.mockClear();
28
- deactivateKeepAwakeMock.mockClear();
29
- activateKeepAwakeAsyncMock.mockResolvedValue(undefined);
30
- deactivateKeepAwakeMock.mockResolvedValue(undefined);
31
- });
32
-
33
- afterEach(() => unmount(ROOT_TAG));
34
-
35
- function mountKeepAwake(tag?: string): void {
36
- mount(
37
- ROOT_TAG,
38
- defineComponent({
39
- setup: () => {
40
- useKeepAwake(tag);
41
- return () => h('symbiote-text', {}, 'keep-awake');
42
- },
43
- }),
44
- );
45
- }
46
-
47
- describe('useKeepAwake (Vue)', () => {
48
- it('activates a default tag on mount', async () => {
49
- mountKeepAwake();
50
-
51
- await vi.waitFor(() => expect(activateKeepAwakeAsyncMock).toHaveBeenCalledTimes(1));
52
- expect(typeof activateKeepAwakeAsyncMock.mock.calls[0][0]).toBe('string');
53
- });
54
-
55
- it('activates the explicit tag when one is given', async () => {
56
- mountKeepAwake('custom-tag');
57
-
58
- await vi.waitFor(() => expect(activateKeepAwakeAsyncMock).toHaveBeenCalledWith('custom-tag'));
59
- });
60
-
61
- it('deactivates the same tag on unmount', async () => {
62
- mountKeepAwake('custom-tag');
63
- await vi.waitFor(() => expect(activateKeepAwakeAsyncMock).toHaveBeenCalledTimes(1));
64
-
65
- unmount(ROOT_TAG);
66
-
67
- expect(deactivateKeepAwakeMock).toHaveBeenCalledWith('custom-tag');
68
- });
69
-
70
- it('never touches addListener when no options are given', async () => {
71
- mountKeepAwake('custom-tag');
72
-
73
- await vi.waitFor(() => expect(activateKeepAwakeAsyncMock).toHaveBeenCalledTimes(1));
74
- expect(addListenerMock).not.toHaveBeenCalled();
75
- });
76
- });