@symbiote-native/keep-awake 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +28 -19
  2. package/build/angular/index.d.ts +1 -1
  3. package/build/angular/index.js +1 -1
  4. package/build/angular/services/keep-awake.service/index.js +7 -6
  5. package/build/core/index.d.ts +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 +1 -1
  11. package/build/solid/index.d.ts +2 -0
  12. package/build/solid/index.js +6 -0
  13. package/build/solid/primitives/create-keep-awake.d.ts +2 -0
  14. package/build/solid/primitives/create-keep-awake.js +41 -0
  15. package/build/svelte/index.d.ts +2 -0
  16. package/build/svelte/index.js +5 -0
  17. package/build/svelte/runes/use-keep-awake.svelte.d.ts +2 -0
  18. package/build/svelte/runes/use-keep-awake.svelte.js +37 -0
  19. package/build/vue/composables/use-keep-awake/index.js +7 -6
  20. package/build/vue/index.d.ts +1 -1
  21. package/build/vue/index.js +1 -1
  22. package/build-ngc/angular/index.d.ts +1 -1
  23. package/build-ngc/angular/index.js +1 -1
  24. package/build-ngc/angular/index.js.map +1 -1
  25. package/build-ngc/angular/services/keep-awake.service/index.js +10 -9
  26. package/build-ngc/angular/services/keep-awake.service/index.js.map +1 -1
  27. package/build-ngc/core/index.d.ts +1 -1
  28. package/build-ngc/core/keep-awake.js.map +1 -1
  29. package/build-ngc/core/listener-attachment.d.ts +8 -0
  30. package/build-ngc/core/listener-attachment.js +19 -0
  31. package/build-ngc/core/listener-attachment.js.map +1 -0
  32. package/build-ngc/core/native-module.js.map +1 -1
  33. package/package.json +42 -14
  34. package/src/angular/index.ts +1 -11
  35. package/src/angular/services/keep-awake.service/index.ts +10 -6
  36. package/src/core/index.ts +5 -1
  37. package/src/core/keep-awake.ts +10 -4
  38. package/src/core/listener-attachment.ts +43 -0
  39. package/src/core/native-module.ts +4 -1
  40. package/src/react/hooks/use-keep-awake/index.ts +16 -17
  41. package/src/react/index.ts +1 -11
  42. package/src/solid/index.ts +7 -0
  43. package/src/solid/primitives/create-keep-awake.ts +54 -0
  44. package/src/svelte/index.ts +6 -0
  45. package/src/svelte/runes/use-keep-awake.svelte.ts +48 -0
  46. package/src/svelte/svelte-ambient.d.ts +6 -0
  47. package/src/vue/composables/use-keep-awake/index.ts +9 -6
  48. package/src/vue/index.ts +1 -11
package/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
 
3
3
  Port of [`expo-keep-awake`](https://docs.expo.dev/versions/latest/sdk/keep-awake/) for
4
4
  [SymbioteNative](../../README.md) — keeps the screen on for the lifetime of a mounted
5
- component/composable/service, reachable from every adapter (React, Vue, Angular), not just
6
- React.
5
+ component/composable/service, reachable from every adapter (React, Vue, Svelte, Solid, Angular),
6
+ not just React.
7
7
 
8
8
  Built the same way as [`@symbiote-native/battery`](../battery) and
9
9
  [`@symbiote-native/device`](../device), an `expo-modules-core`-based wrapper (see the
@@ -31,12 +31,12 @@ into the native host app **once**, covering this package and every other `expo-m
31
31
  package (`@symbiote-native/battery`, `@symbiote-native/device`, `@symbiote-native/sensors`, …)
32
32
  with zero further changes:
33
33
 
34
- | Platform | Touches |
35
- |---|---|
36
- | iOS | `ios/Podfile` — add `use_expo_modules!` |
37
- | iOS | `AppDelegate.swift` — Expo's runtime-bootstrap hook |
38
- | Android | `settings.gradle` / `app/build.gradle` — resolve and include the Expo Gradle projects |
39
- | Android | `MainApplication.kt` — Expo's bootstrap hook, plus a hand-written native-module name map (there's no `expo` meta-package here to auto-generate one) |
34
+ | Platform | Touches |
35
+ | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
36
+ | iOS | `ios/Podfile` — add `use_expo_modules!` |
37
+ | iOS | `AppDelegate.swift` — Expo's runtime-bootstrap hook |
38
+ | Android | `settings.gradle` / `app/build.gradle` — resolve and include the Expo Gradle projects |
39
+ | Android | `MainApplication.kt` — Expo's bootstrap hook, plus a hand-written native-module name map (there's no `expo` meta-package here to auto-generate one) |
40
40
 
41
41
  The recurring per-package half of the Android wiring (`app/build.gradle`'s
42
42
  `implementation project(':expo-keep-awake')` line and `MainApplication.kt`'s module-name map
@@ -59,15 +59,18 @@ src/core/ keep-awake.ts — isAvailableAsync/activateKeepAwakeAsyn
59
59
  KeepAwakeOptions, hand-ported from KeepAwake.types.ts.
60
60
  src/react/hooks/ @symbiote-native/keep-awake/react — useKeepAwake
61
61
  src/vue/composables/ @symbiote-native/keep-awake/vue — useKeepAwake
62
+ src/svelte/runes/ @symbiote-native/keep-awake/svelte — useKeepAwake
63
+ src/solid/primitives/ @symbiote-native/keep-awake/solid — createKeepAwake (Solid says
64
+ create*, not use*)
62
65
  src/angular/services/ @symbiote-native/keep-awake/angular — KeepAwakeService (`.connect()`,
63
66
  no return value — a pure side effect for the component's lifetime)
64
67
  ```
65
68
 
66
- Each adapter's hook/composable/service activates a keep-awake lock on mount and deactivates it on
67
- unmount, over the same `core` functions — the tag-generation and activate/deactivate lifecycle is
68
- written once and shared by all three. React uses `useId()` for its default per-instance tag,
69
- matching upstream; Vue and Angular have no `useId` equivalent, so both fall back to a small
70
- monotonically-incrementing module-local counter (`keep-awake-tag-1`, `keep-awake-tag-2`, …) when
69
+ Each adapter's hook/composable/rune/primitive/service activates a keep-awake lock on mount and
70
+ deactivates it on unmount, over the same `core` functions — the tag-generation and
71
+ activate/deactivate lifecycle is written once and shared by every adapter. React uses `useId()`
72
+ for its default per-instance tag, matching upstream; Vue, Svelte, Solid and Angular have no
73
+ `useId` equivalent, so they fall back to a small monotonically-incrementing module-local counter (`keep-awake-tag-1`, `keep-awake-tag-2`, …) when
71
74
  no explicit tag is given.
72
75
 
73
76
  ## Use it
@@ -100,7 +103,7 @@ useKeepAwake();
100
103
  import { Component, inject } from '@angular/core';
101
104
  import { KeepAwakeService } from '@symbiote-native/keep-awake/angular';
102
105
 
103
- @Component({ /* ... */ })
106
+ @Component({/* ... */})
104
107
  export class KeepAwakeScreen {
105
108
  constructor() {
106
109
  inject(KeepAwakeService).connect();
@@ -123,25 +126,31 @@ as a minimal `unknown` placeholder: upstream's own shape there is web-specific a
123
126
  analogue, and native listeners for it rarely fire.
124
127
 
125
128
  ```ts
126
- import { activateKeepAwakeAsync, deactivateKeepAwake } from '@symbiote-native/keep-awake';
129
+ import {
130
+ activateKeepAwakeAsync,
131
+ deactivateKeepAwake,
132
+ } from '@symbiote-native/keep-awake';
127
133
 
128
134
  // framework-scoped entry points re-export the same free functions, plus a lifecycle
129
135
  // hook/composable/service:
130
136
  import { useKeepAwake } from '@symbiote-native/keep-awake/react';
131
137
  import { useKeepAwake } from '@symbiote-native/keep-awake/vue';
138
+ import { useKeepAwake } from '@symbiote-native/keep-awake/svelte';
139
+ import { createKeepAwake } from '@symbiote-native/keep-awake/solid';
132
140
  import { KeepAwakeService } from '@symbiote-native/keep-awake/angular';
133
141
  ```
134
142
 
135
- Each hook/composable/service activates a lock on mount (optionally registering `options.listener`
136
- once activation resolves) and deactivates it on unmount — mirroring upstream's own
137
- `useKeepAwake(tag?, options?)`.
143
+ Each hook/composable/rune/primitive/service activates a lock on mount (optionally registering
144
+ `options.listener` once activation resolves) and deactivates it on unmount — mirroring upstream's
145
+ own `useKeepAwake(tag?, options?)`. Solid's `createKeepAwake(tag?, options?)` does it from the
146
+ primitive body, releasing through `onCleanup`.
138
147
 
139
148
  ## Test it
140
149
 
141
150
  No Fabric/Descriptor angle at all — keep-awake is a pure async-function + `EventEmitter` listener
142
151
  surface, never a view. Tests inject a fake native-module object in place of the real
143
152
  `requireNativeModule` resolution (`src/core/keep-awake.test.ts`,
144
- `src/{react,vue,angular}/**/*.test.{ts,tsx}`, `vitest`), the same pattern
153
+ `src/{react,vue,svelte,solid,angular}/**/*.test.{ts,tsx}`, `vitest`), the same pattern
145
154
  `@symbiote-native/battery` and `@symbiote-native/device` use — no `installFabric()`-driven Fabric
146
155
  assertions on the core level, only the adapter lifecycle tests mount through the fake Fabric slot
147
156
  to prove activate-on-mount/deactivate-on-unmount. Native rendering itself is verified on-device —
@@ -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
3
  export { KeepAwakeService } from './services/keep-awake.service/index.js';
4
- export { ExpoKeepAwakeTag, isAvailableAsync, activateKeepAwakeAsync, deactivateKeepAwake, addListener, } from '../core/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/index.js';
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,3 +1,3 @@
1
1
  export { ExpoKeepAwakeTag, isAvailableAsync, activateKeepAwakeAsync, deactivateKeepAwake, addListener, } from './keep-awake';
2
- export type { KeepAwakeEvent, KeepAwakeListener, KeepAwakeOptions } from './types';
2
+ export type { KeepAwakeEvent, KeepAwakeListener, KeepAwakeOptions, } from './types';
3
3
  export type { EventSubscription } from 'expo-modules-core';
@@ -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/index.js';
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';
@@ -2,4 +2,4 @@
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
4
  export { useKeepAwake } from './hooks/use-keep-awake/index.js';
5
- export { ExpoKeepAwakeTag, isAvailableAsync, activateKeepAwakeAsync, deactivateKeepAwake, addListener, } from '../core/index.js';
5
+ export * from '../core/index.js';
@@ -0,0 +1,2 @@
1
+ export { createKeepAwake } from './primitives/create-keep-awake';
2
+ export * from '../core';
@@ -0,0 +1,6 @@
1
+ // @symbiote-native/keep-awake/solid: the Solid entry over the framework-agnostic core. The
2
+ // lifecycle bucket is `primitives/` and the name is `create*` (never `hooks/`/`use*`, that is
3
+ // React's term and Solid's word for consuming an existing thing) — see
4
+ // adapters/solid/src/primitives for the same convention.
5
+ export { createKeepAwake } from './primitives/create-keep-awake.js';
6
+ export * from '../core/index.js';
@@ -0,0 +1,2 @@
1
+ import { type KeepAwakeOptions } from '../../core';
2
+ export declare function createKeepAwake(tag?: string, options?: KeepAwakeOptions): void;
@@ -0,0 +1,41 @@
1
+ // Solid lifecycle wiring over the framework-agnostic core (core/keep-awake.ts): activates a
2
+ // keep-awake lock in the primitive body, deactivates it when the owner is disposed — the Solid
3
+ // twin of React's hook, Vue's composable, Svelte's rune and Angular's KeepAwakeService.connect().
4
+ //
5
+ // `primitives/` and `create*`, not `hooks/`/`use*`: Solid's ecosystem calls a composable reactive
6
+ // function a PRIMITIVE and reserves `use*` for consuming something that already exists. Full
7
+ // rationale in adapters/solid/src/primitives/create-color-scheme.ts's header.
8
+ //
9
+ // Activation runs SYNCHRONOUSLY in the body with `onCleanup` for teardown, not from a mount hook
10
+ // (React's useEffect, Vue's onMounted, Svelte's $effect) — the lock is requested one tick earlier,
11
+ // and nothing can interleave between construction and the request.
12
+ //
13
+ // Nothing reactive is returned: keeping the screen awake is a pure side effect for the owner's
14
+ // lifetime, so there is no accessor and the once-per-owner contract matches Vue/Svelte/Angular
15
+ // rather than React's hook, which re-runs when the tag changes.
16
+ //
17
+ // No `useId` in Solid, so the default tag comes from a module-local counter, as in Vue and Svelte.
18
+ //
19
+ // Outside a component / createRoot there is no owner for `onCleanup`, so the lock is never
20
+ // released — see create-color-scheme's header for the same documented caveat.
21
+ import { onCleanup } from 'solid-js';
22
+ import { activateKeepAwakeAsync, deactivateKeepAwake, } from '../../core/index.js';
23
+ import { createKeepAwakeListenerAttachment } from '../../core/listener-attachment.js';
24
+ let tagCounter = 0;
25
+ export function createKeepAwake(tag, options) {
26
+ const tagOrDefault = tag ?? `keep-awake-tag-${++tagCounter}`;
27
+ // One attachment per owner: the body runs once, so it spans the single activate/onCleanup pair.
28
+ const attachment = createKeepAwakeListenerAttachment(tagOrDefault, options?.listener);
29
+ activateKeepAwakeAsync(tagOrDefault)
30
+ .then(() => attachment.attach())
31
+ .catch(() => { });
32
+ onCleanup(() => {
33
+ attachment.release();
34
+ if (options?.suppressDeactivateWarnings) {
35
+ deactivateKeepAwake(tagOrDefault).catch(() => { });
36
+ }
37
+ else {
38
+ deactivateKeepAwake(tagOrDefault);
39
+ }
40
+ });
41
+ }
@@ -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/index.js';
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';
@@ -2,4 +2,4 @@
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
4
  export { useKeepAwake } from './composables/use-keep-awake/index.js';
5
- export { ExpoKeepAwakeTag, isAvailableAsync, activateKeepAwakeAsync, deactivateKeepAwake, addListener, } from '../core/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,EACL,sBAAsB,EACtB,mBAAmB,GAEpB,MAAM,eAAe,CAAC;AACvB,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,CAClD,YAAY,EACZ,OAAO,EAAE,QAAQ,CAClB,CAAC;YAEF,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;uGA9BU,gBAAgB;2GAAhB,gBAAgB,cADH,MAAM;;2FACnB,gBAAgB;kBAD5B,UAAU;mBAAC,EAAE,UAAU,EAAE,MAAM,EAAE"}
@@ -1,3 +1,3 @@
1
1
  export { ExpoKeepAwakeTag, isAvailableAsync, activateKeepAwakeAsync, deactivateKeepAwake, addListener, } from './keep-awake';
2
- export type { KeepAwakeEvent, KeepAwakeListener, KeepAwakeOptions } from './types';
2
+ export type { KeepAwakeEvent, KeepAwakeListener, KeepAwakeOptions, } from './types';
3
3
  export type { EventSubscription } from 'expo-modules-core';
@@ -1 +1 @@
1
- {"version":3,"file":"keep-awake.js","sourceRoot":"","sources":["../../src/core/keep-awake.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,oFAAoF;AACpF,2FAA2F;AAC3F,6EAA6E;AAC7E,qFAAqF;AACrF,OAAO,EAAE,mBAAmB,EAA0B,MAAM,mBAAmB,CAAC;AAChF,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD,MAAM,CAAC,MAAM,gBAAgB,GAAG,yBAAyB,CAAC;AAE1D,4EAA4E;AAC5E,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,IAAI,aAAa,CAAC,gBAAgB,EAAE,CAAC;QACnC,OAAO,MAAM,aAAa,CAAC,gBAAgB,EAAE,CAAC;IAChD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,MAAc,gBAAgB;IACzE,MAAM,aAAa,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC;AACtC,CAAC;AAED,iGAAiG;AACjG,MAAM,CAAC,KAAK,UAAU,mBAAmB,CAAC,MAAc,gBAAgB;IACtE,MAAM,aAAa,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,CAAC;AACxC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CACzB,aAAyC,EACzC,QAA4B;IAE5B,IAAI,CAAC,aAAa,CAAC,iBAAiB,EAAE,CAAC;QACrC,MAAM,IAAI,mBAAmB,CAAC,eAAe,EAAE,mBAAmB,CAAC,CAAC;IACtE,CAAC;IACD,MAAM,GAAG,GAAG,OAAO,aAAa,KAAK,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,gBAAgB,CAAC;IACjF,MAAM,gBAAgB,GAAG,OAAO,aAAa,KAAK,UAAU,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC;IACxF,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,MAAM,IAAI,SAAS,CAAC,0CAA0C,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,aAAa,CAAC,iBAAiB,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC;AAChE,CAAC"}
1
+ {"version":3,"file":"keep-awake.js","sourceRoot":"","sources":["../../src/core/keep-awake.ts"],"names":[],"mappings":"AAAA,0FAA0F;AAC1F,oFAAoF;AACpF,2FAA2F;AAC3F,6EAA6E;AAC7E,qFAAqF;AACrF,OAAO,EAAE,mBAAmB,EAA0B,MAAM,mBAAmB,CAAC;AAChF,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAGhD,MAAM,CAAC,MAAM,gBAAgB,GAAG,yBAAyB,CAAC;AAE1D,4EAA4E;AAC5E,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,IAAI,aAAa,CAAC,gBAAgB,EAAE,CAAC;QACnC,OAAO,MAAM,aAAa,CAAC,gBAAgB,EAAE,CAAC;IAChD,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,MAAc,gBAAgB;IAE9B,MAAM,aAAa,CAAC,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC;AACtC,CAAC;AAED,iGAAiG;AACjG,MAAM,CAAC,KAAK,UAAU,mBAAmB,CACvC,MAAc,gBAAgB;IAE9B,MAAM,aAAa,CAAC,UAAU,EAAE,CAAC,GAAG,CAAC,CAAC;AACxC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,WAAW,CACzB,aAAyC,EACzC,QAA4B;IAE5B,IAAI,CAAC,aAAa,CAAC,iBAAiB,EAAE,CAAC;QACrC,MAAM,IAAI,mBAAmB,CAAC,eAAe,EAAE,mBAAmB,CAAC,CAAC;IACtE,CAAC;IACD,MAAM,GAAG,GACP,OAAO,aAAa,KAAK,QAAQ,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,gBAAgB,CAAC;IACvE,MAAM,gBAAgB,GACpB,OAAO,aAAa,KAAK,UAAU,CAAC,CAAC,CAAC,aAAa,CAAC,CAAC,CAAC,QAAQ,CAAC;IACjE,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACtB,MAAM,IAAI,SAAS,CAAC,0CAA0C,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,aAAa,CAAC,iBAAiB,CAAC,GAAG,EAAE,gBAAgB,CAAC,CAAC;AAChE,CAAC"}
@@ -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"}
@@ -1 +1 @@
1
- {"version":3,"file":"native-module.js","sourceRoot":"","sources":["../../src/core/native-module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAIxD,MAAM,2BAA2B,GAAG,eAAe,CAAC;AAapD,MAAM,CAAC,MAAM,aAAa,GAAG,mBAAmB,CAC9C,2BAA2B,CAC5B,CAAC"}
1
+ {"version":3,"file":"native-module.js","sourceRoot":"","sources":["../../src/core/native-module.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAIxD,MAAM,2BAA2B,GAAG,eAAe,CAAC;AAgBpD,MAAM,CAAC,MAAM,aAAa,GAAG,mBAAmB,CAC9C,2BAA2B,CAC5B,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@symbiote-native/keep-awake",
3
- "version": "0.1.0",
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.",
3
+ "version": "0.3.0",
4
+ "description": "expo-keep-awake wrapped for SymbioteNative — one framework-agnostic core, built once and reachable from the React, Vue, Svelte, Solid, 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": {
7
7
  "type": "git",
@@ -30,6 +30,14 @@
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
+ },
37
+ "./solid": {
38
+ "types": "./build/solid/index.d.ts",
39
+ "default": "./build/solid/index.js"
40
+ },
33
41
  "./angular": {
34
42
  "types": "./build-ngc/angular/index.d.ts",
35
43
  "react-native": "./build-ngc/angular/index.js",
@@ -53,15 +61,19 @@
53
61
  "expo-modules-core": "57.0.5"
54
62
  },
55
63
  "peerDependencies": {
64
+ "@symbiote-native/angular": ">=0.6.2",
56
65
  "@symbiote-native/engine": ">=0.1.7",
66
+ "@symbiote-native/react": ">=0.2.8",
67
+ "@symbiote-native/solid": ">=0.0.1",
68
+ "@symbiote-native/svelte": ">=0.1.0",
69
+ "@symbiote-native/vue": ">=0.3.8",
57
70
  "@angular/core": ">=20",
58
71
  "@vue/runtime-core": "^3.5.13",
59
72
  "react": ">=19.0.0",
60
73
  "react-native": ">=0.86",
61
- "vue": ">=3.5.0",
62
- "@symbiote-native/angular": "0.6.2",
63
- "@symbiote-native/react": "0.2.8",
64
- "@symbiote-native/vue": "0.3.8"
74
+ "solid-js": ">=1.9.0",
75
+ "svelte": ">=5.56.0",
76
+ "vue": ">=3.5.0"
65
77
  },
66
78
  "peerDependenciesMeta": {
67
79
  "@symbiote-native/angular": {
@@ -70,6 +82,12 @@
70
82
  "@symbiote-native/react": {
71
83
  "optional": true
72
84
  },
85
+ "@symbiote-native/solid": {
86
+ "optional": true
87
+ },
88
+ "@symbiote-native/svelte": {
89
+ "optional": true
90
+ },
73
91
  "@symbiote-native/vue": {
74
92
  "optional": true
75
93
  },
@@ -82,24 +100,34 @@
82
100
  "react": {
83
101
  "optional": true
84
102
  },
103
+ "solid-js": {
104
+ "optional": true
105
+ },
106
+ "svelte": {
107
+ "optional": true
108
+ },
85
109
  "vue": {
86
110
  "optional": true
87
111
  }
88
112
  },
89
113
  "devDependencies": {
90
- "@angular/compiler": "^22",
91
- "@angular/compiler-cli": "^22",
92
- "@angular/core": "^22",
114
+ "@angular/compiler": "~22.0.8",
115
+ "@angular/compiler-cli": "~22.0.8",
116
+ "@angular/core": "~22.0.8",
93
117
  "@types/node": "^26.0.0",
94
118
  "@types/react": "^19.2.0",
95
119
  "@vue/runtime-core": "^3.5.13",
96
120
  "react": "19.2.3",
121
+ "solid-js": "^1.9.14",
122
+ "svelte": "^5.56.0",
97
123
  "typescript": "~6.0.0",
98
- "@symbiote-native/angular": "0.6.2",
99
- "@symbiote-native/engine": "0.1.7",
100
- "@symbiote-native/react": "0.2.8",
101
- "@symbiote-native/test-utils": "0.1.6",
102
- "@symbiote-native/vue": "0.3.8"
124
+ "@symbiote-native/angular": "0.8.0",
125
+ "@symbiote-native/engine": "0.3.0",
126
+ "@symbiote-native/react": "0.4.0",
127
+ "@symbiote-native/solid": "0.1.0",
128
+ "@symbiote-native/svelte": "0.3.0",
129
+ "@symbiote-native/test-utils": "0.2.0",
130
+ "@symbiote-native/vue": "0.5.0"
103
131
  },
104
132
  "scripts": {
105
133
  "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,10 @@
1
1
  import { effect, inject, Injectable, Injector } from '@angular/core';
2
2
  import {
3
3
  activateKeepAwakeAsync,
4
- addListener,
5
4
  deactivateKeepAwake,
6
5
  type KeepAwakeOptions,
7
6
  } from '../../../core';
7
+ import { createKeepAwakeListenerAttachment } from '../../../core/listener-attachment';
8
8
 
9
9
  // Angular twin of React's `useKeepAwake` hook and Vue's `useKeepAwake` composable. See
10
10
  // LowPowerModeService (packages/battery) for the `connect()`/`effect()` pattern rationale — same
@@ -24,15 +24,19 @@ export class KeepAwakeService {
24
24
 
25
25
  effect(
26
26
  onCleanup => {
27
+ // Per effect run: onCleanup releases this exact attachment, so a re-run always starts
28
+ // from a fresh one rather than reviving a released attach.
29
+ const attachment = createKeepAwakeListenerAttachment(
30
+ tagOrDefault,
31
+ options?.listener,
32
+ );
33
+
27
34
  activateKeepAwakeAsync(tagOrDefault)
28
- .then(() => {
29
- if (options?.listener) {
30
- addListener(tagOrDefault, options.listener);
31
- }
32
- })
35
+ .then(() => attachment.attach())
33
36
  .catch(() => {});
34
37
 
35
38
  onCleanup(() => {
39
+ attachment.release();
36
40
  if (options?.suppressDeactivateWarnings) {
37
41
  deactivateKeepAwake(tagOrDefault).catch(() => {});
38
42
  } else {
package/src/core/index.ts CHANGED
@@ -5,5 +5,9 @@ export {
5
5
  deactivateKeepAwake,
6
6
  addListener,
7
7
  } from './keep-awake';
8
- export type { KeepAwakeEvent, KeepAwakeListener, KeepAwakeOptions } from './types';
8
+ export type {
9
+ KeepAwakeEvent,
10
+ KeepAwakeListener,
11
+ KeepAwakeOptions,
12
+ } from './types';
9
13
  export type { EventSubscription } from 'expo-modules-core';
@@ -21,12 +21,16 @@ export async function isAvailableAsync(): Promise<boolean> {
21
21
  * Activates a keep-awake lock under `tag` (the shared default tag when none is given) — the
22
22
  * screen stays on for as long as any tag holds an active lock.
23
23
  */
24
- export async function activateKeepAwakeAsync(tag: string = ExpoKeepAwakeTag): Promise<void> {
24
+ export async function activateKeepAwakeAsync(
25
+ tag: string = ExpoKeepAwakeTag,
26
+ ): Promise<void> {
25
27
  await expoKeepAwake.activate?.(tag);
26
28
  }
27
29
 
28
30
  /** Releases the keep-awake lock held under `tag` (the shared default tag when none is given). */
29
- export async function deactivateKeepAwake(tag: string = ExpoKeepAwakeTag): Promise<void> {
31
+ export async function deactivateKeepAwake(
32
+ tag: string = ExpoKeepAwakeTag,
33
+ ): Promise<void> {
30
34
  await expoKeepAwake.deactivate?.(tag);
31
35
  }
32
36
 
@@ -41,8 +45,10 @@ export function addListener(
41
45
  if (!expoKeepAwake.addListenerForTag) {
42
46
  throw new UnavailabilityError('ExpoKeepAwake', 'addListenerForTag');
43
47
  }
44
- const tag = typeof tagOrListener === 'string' ? tagOrListener : ExpoKeepAwakeTag;
45
- const resolvedListener = typeof tagOrListener === 'function' ? tagOrListener : listener;
48
+ const tag =
49
+ typeof tagOrListener === 'string' ? tagOrListener : ExpoKeepAwakeTag;
50
+ const resolvedListener =
51
+ typeof tagOrListener === 'function' ? tagOrListener : listener;
46
52
  if (!resolvedListener) {
47
53
  throw new TypeError('addListener requires a listener function');
48
54
  }
@@ -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
+ }
@@ -12,7 +12,10 @@ export type INativeKeepAwakeModule = {
12
12
  isAvailableAsync?(): Promise<boolean>;
13
13
  activate?(tag: string): Promise<void>;
14
14
  deactivate?(tag: string): Promise<void>;
15
- addListenerForTag?(tag: string, listener: KeepAwakeListener): EventSubscription;
15
+ addListenerForTag?(
16
+ tag: string,
17
+ listener: KeepAwakeListener,
18
+ ): EventSubscription;
16
19
  };
17
20
 
18
21
  export const expoKeepAwake = requireNativeModule<INativeKeepAwakeModule>(
@@ -1,38 +1,37 @@
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
10
  import {
13
11
  activateKeepAwakeAsync,
14
- addListener,
15
12
  deactivateKeepAwake,
16
13
  type KeepAwakeOptions,
17
14
  } from '../../../core';
15
+ import { createKeepAwakeListenerAttachment } from '../../../core/listener-attachment';
18
16
 
19
17
  export function useKeepAwake(tag?: string, options?: KeepAwakeOptions): void {
20
18
  const defaultTag = useId();
21
19
  const tagOrDefault = tag ?? defaultTag;
22
20
 
23
21
  useEffect(() => {
24
- let isMounted = true;
22
+ // Per effect run, not per hook call - a changed tag tears the old attachment down with the
23
+ // rest of the cleanup and starts a fresh one.
24
+ const attachment = createKeepAwakeListenerAttachment(
25
+ tagOrDefault,
26
+ options?.listener,
27
+ );
25
28
 
26
29
  activateKeepAwakeAsync(tagOrDefault)
27
- .then(() => {
28
- if (isMounted && options?.listener) {
29
- addListener(tagOrDefault, options.listener);
30
- }
31
- })
30
+ .then(() => attachment.attach())
32
31
  .catch(() => {});
33
32
 
34
33
  return () => {
35
- isMounted = false;
34
+ attachment.release();
36
35
  if (options?.suppressDeactivateWarnings) {
37
36
  deactivateKeepAwake(tagOrDefault).catch(() => {});
38
37
  } 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,7 @@
1
+ // @symbiote-native/keep-awake/solid: the Solid entry over the framework-agnostic core. The
2
+ // lifecycle bucket is `primitives/` and the name is `create*` (never `hooks/`/`use*`, that is
3
+ // React's term and Solid's word for consuming an existing thing) — see
4
+ // adapters/solid/src/primitives for the same convention.
5
+
6
+ export { createKeepAwake } from './primitives/create-keep-awake';
7
+ export * from '../core';
@@ -0,0 +1,54 @@
1
+ // Solid lifecycle wiring over the framework-agnostic core (core/keep-awake.ts): activates a
2
+ // keep-awake lock in the primitive body, deactivates it when the owner is disposed — the Solid
3
+ // twin of React's hook, Vue's composable, Svelte's rune and Angular's KeepAwakeService.connect().
4
+ //
5
+ // `primitives/` and `create*`, not `hooks/`/`use*`: Solid's ecosystem calls a composable reactive
6
+ // function a PRIMITIVE and reserves `use*` for consuming something that already exists. Full
7
+ // rationale in adapters/solid/src/primitives/create-color-scheme.ts's header.
8
+ //
9
+ // Activation runs SYNCHRONOUSLY in the body with `onCleanup` for teardown, not from a mount hook
10
+ // (React's useEffect, Vue's onMounted, Svelte's $effect) — the lock is requested one tick earlier,
11
+ // and nothing can interleave between construction and the request.
12
+ //
13
+ // Nothing reactive is returned: keeping the screen awake is a pure side effect for the owner's
14
+ // lifetime, so there is no accessor and the once-per-owner contract matches Vue/Svelte/Angular
15
+ // rather than React's hook, which re-runs when the tag changes.
16
+ //
17
+ // No `useId` in Solid, so the default tag comes from a module-local counter, as in Vue and Svelte.
18
+ //
19
+ // Outside a component / createRoot there is no owner for `onCleanup`, so the lock is never
20
+ // released — see create-color-scheme's header for the same documented caveat.
21
+ import { onCleanup } from 'solid-js';
22
+ import {
23
+ activateKeepAwakeAsync,
24
+ deactivateKeepAwake,
25
+ type KeepAwakeOptions,
26
+ } from '../../core';
27
+ import { createKeepAwakeListenerAttachment } from '../../core/listener-attachment';
28
+
29
+ let tagCounter = 0;
30
+
31
+ export function createKeepAwake(
32
+ tag?: string,
33
+ options?: KeepAwakeOptions,
34
+ ): void {
35
+ const tagOrDefault = tag ?? `keep-awake-tag-${++tagCounter}`;
36
+ // One attachment per owner: the body runs once, so it spans the single activate/onCleanup pair.
37
+ const attachment = createKeepAwakeListenerAttachment(
38
+ tagOrDefault,
39
+ options?.listener,
40
+ );
41
+
42
+ activateKeepAwakeAsync(tagOrDefault)
43
+ .then(() => attachment.attach())
44
+ .catch(() => {});
45
+
46
+ onCleanup(() => {
47
+ attachment.release();
48
+ if (options?.suppressDeactivateWarnings) {
49
+ deactivateKeepAwake(tagOrDefault).catch(() => {});
50
+ } else {
51
+ deactivateKeepAwake(tagOrDefault);
52
+ }
53
+ });
54
+ }
@@ -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,48 @@
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 {
17
+ activateKeepAwakeAsync,
18
+ deactivateKeepAwake,
19
+ type KeepAwakeOptions,
20
+ } from '../../core';
21
+ import { createKeepAwakeListenerAttachment } from '../../core/listener-attachment';
22
+
23
+ let tagCounter = 0;
24
+
25
+ export function useKeepAwake(tag?: string, options?: KeepAwakeOptions): void {
26
+ const tagOrDefault = tag ?? `keep-awake-tag-${++tagCounter}`;
27
+
28
+ $effect(() => {
29
+ // Inside the effect, so the attachment shares the teardown returned below.
30
+ const attachment = createKeepAwakeListenerAttachment(
31
+ tagOrDefault,
32
+ options?.listener,
33
+ );
34
+
35
+ activateKeepAwakeAsync(tagOrDefault)
36
+ .then(() => attachment.attach())
37
+ .catch(() => {});
38
+
39
+ return () => {
40
+ attachment.release();
41
+ if (options?.suppressDeactivateWarnings) {
42
+ deactivateKeepAwake(tagOrDefault).catch(() => {});
43
+ } else {
44
+ deactivateKeepAwake(tagOrDefault);
45
+ }
46
+ };
47
+ });
48
+ }
@@ -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" />
@@ -6,27 +6,30 @@
6
6
  import { onMounted, onUnmounted } from '@vue/runtime-core';
7
7
  import {
8
8
  activateKeepAwakeAsync,
9
- addListener,
10
9
  deactivateKeepAwake,
11
10
  type KeepAwakeOptions,
12
11
  } from '../../../core';
12
+ import { createKeepAwakeListenerAttachment } from '../../../core/listener-attachment';
13
13
 
14
14
  let tagCounter = 0;
15
15
 
16
16
  export function useKeepAwake(tag?: string, options?: KeepAwakeOptions): void {
17
17
  const tagOrDefault = tag ?? `keep-awake-tag-${++tagCounter}`;
18
+ // Setup body scope: it runs once per instance, so one attachment spans the single
19
+ // onMounted/onUnmounted pair - no need to re-create it on the mount hook.
20
+ const attachment = createKeepAwakeListenerAttachment(
21
+ tagOrDefault,
22
+ options?.listener,
23
+ );
18
24
 
19
25
  onMounted(() => {
20
26
  activateKeepAwakeAsync(tagOrDefault)
21
- .then(() => {
22
- if (options?.listener) {
23
- addListener(tagOrDefault, options.listener);
24
- }
25
- })
27
+ .then(() => attachment.attach())
26
28
  .catch(() => {});
27
29
  });
28
30
 
29
31
  onUnmounted(() => {
32
+ attachment.release();
30
33
  if (options?.suppressDeactivateWarnings) {
31
34
  deactivateKeepAwake(tagOrDefault).catch(() => {});
32
35
  } 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';