@elizaos/capacitor-eliza-tasks 2.0.3-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ElizaosCapacitorElizaTasks.podspec +19 -0
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/dist/esm/definitions.d.ts +84 -0
- package/dist/esm/definitions.d.ts.map +1 -0
- package/dist/esm/definitions.js +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.d.ts.map +1 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/web.d.ts +17 -0
- package/dist/esm/web.d.ts.map +1 -0
- package/dist/esm/web.js +32 -0
- package/dist/esm/web.test.d.ts +2 -0
- package/dist/esm/web.test.d.ts.map +1 -0
- package/dist/esm/web.test.js +34 -0
- package/dist/plugin.cjs.js +48 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +51 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/ElizaTasksPlugin/ElizaTasksPlugin.swift +326 -0
- package/package.json +84 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'ElizaosCapacitorElizaTasks'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.license = package['license'] || { :type => 'MIT' }
|
|
10
|
+
s.homepage = 'https://elizaos.ai'
|
|
11
|
+
s.authors = { 'elizaOS' => 'dev@elizaos.ai' }
|
|
12
|
+
s.source = { :git => 'https://github.com/elizaOS/eliza.git', :tag => s.version.to_s }
|
|
13
|
+
s.source_files = 'ios/Sources/**/*.{swift,h,m}'
|
|
14
|
+
s.ios.deployment_target = '15.0'
|
|
15
|
+
s.dependency 'Capacitor'
|
|
16
|
+
s.swift_version = '5.9'
|
|
17
|
+
# BackgroundTasks ships in the iOS SDK; only the framework needs declaring.
|
|
18
|
+
s.frameworks = 'UIKit', 'BackgroundTasks', 'UserNotifications'
|
|
19
|
+
end
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shaw Walters and elizaOS Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# @elizaos/capacitor-eliza-tasks
|
|
2
|
+
|
|
3
|
+
Capacitor plugin that bridges iOS `BGTaskScheduler` background-wake events into the elizaOS Capacitor runtime.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
iOS suspends apps when they leave the foreground. This plugin registers two background task identifiers with `BGTaskScheduler` so the elizaOS iOS app can be woken by the OS on a schedule:
|
|
8
|
+
|
|
9
|
+
- **`BGAppRefreshTask`** (`ai.eliza.tasks.refresh`) — short wake (~25s budget), network available. Used for polling the agent's loopback `/api/internal/wake` route.
|
|
10
|
+
- **`BGProcessingTask`** (`ai.eliza.tasks.processing`) — long-running wake (~120s budget), requires device to be charging and idle. Used for local-LLM warmup passes.
|
|
11
|
+
- **Silent APNs push** (`remote-push`) — optional; gated on `ELIZA_APNS_ENABLED` in `Info.plist`. Forwarded through `AppDelegate` via `ElizaCompanionRemotePush` `NSNotification`.
|
|
12
|
+
|
|
13
|
+
All three wake paths emit the same `wake` event to the JS layer, so a single handler can drain them:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { ElizaTasks } from "@elizaos/capacitor-eliza-tasks";
|
|
17
|
+
|
|
18
|
+
await ElizaTasks.addListener("wake", (event) => {
|
|
19
|
+
console.log(event.kind, event.deadlineSec, event.firedAtMs);
|
|
20
|
+
// event.kind: "refresh" | "processing" | "remote-push"
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Arm the first wake
|
|
24
|
+
await ElizaTasks.scheduleNext({ earliestBeginSec: 900 });
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
On web and non-iOS platforms the plugin returns `supported: false`; scheduling reports no iOS wake path, and cancellation reports that no web wake requests were cancelled. The app should call `getStatus()` and fall back to `@capacitor/background-runner` polling when `supported` is false.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
npm install @elizaos/capacitor-eliza-tasks
|
|
33
|
+
npx cap sync
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### iOS setup
|
|
37
|
+
|
|
38
|
+
Add both identifiers to `Info.plist`:
|
|
39
|
+
|
|
40
|
+
```xml
|
|
41
|
+
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
|
42
|
+
<array>
|
|
43
|
+
<string>ai.eliza.tasks.refresh</string>
|
|
44
|
+
<string>ai.eliza.tasks.processing</string>
|
|
45
|
+
</array>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
For silent-push support also add:
|
|
49
|
+
|
|
50
|
+
```xml
|
|
51
|
+
<key>ELIZA_APNS_ENABLED</key>
|
|
52
|
+
<string>1</string>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
And enable the **Background Modes** capability in Xcode: check `Background fetch` and `Remote notifications`.
|
|
56
|
+
|
|
57
|
+
## API
|
|
58
|
+
|
|
59
|
+
### `scheduleNext(options?)`
|
|
60
|
+
|
|
61
|
+
Enqueues the next `BGAppRefreshTask`. Idempotent — replaces any pending request.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
interface ElizaTasksScheduleOptions {
|
|
65
|
+
earliestBeginSec?: number; // default 900 (15 min), floor 1
|
|
66
|
+
alsoProcessing?: boolean; // also schedule a BGProcessingTask
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Returns `ElizaTasksScheduleResult` with `{ scheduled, identifier, earliestBeginAtMs, reason }`.
|
|
71
|
+
|
|
72
|
+
### `getStatus()`
|
|
73
|
+
|
|
74
|
+
Returns the plugin's view of `BGTaskScheduler` state:
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
interface ElizaTasksStatus {
|
|
78
|
+
supported: boolean;
|
|
79
|
+
platform: "ios" | "android" | "web";
|
|
80
|
+
refreshScheduled: boolean;
|
|
81
|
+
processingScheduled: boolean;
|
|
82
|
+
lastWakeFiredAtMs: number | null;
|
|
83
|
+
lastWakeKind: ElizaTaskKind | null;
|
|
84
|
+
reason: string | null;
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### `cancelAll()`
|
|
89
|
+
|
|
90
|
+
Cancels all pending refresh and processing task requests.
|
|
91
|
+
|
|
92
|
+
### `addListener("wake", fn)`
|
|
93
|
+
|
|
94
|
+
Registers a listener for `ElizaTasksWakeEvent`:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
interface ElizaTasksWakeEvent {
|
|
98
|
+
kind: "refresh" | "processing" | "remote-push";
|
|
99
|
+
identifier: string;
|
|
100
|
+
deadlineSec: number;
|
|
101
|
+
firedAtMs: number;
|
|
102
|
+
payload: Record<string, unknown>;
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### `removeAllListeners()`
|
|
107
|
+
|
|
108
|
+
Removes all `wake` listeners.
|
|
109
|
+
|
|
110
|
+
## Platform support
|
|
111
|
+
|
|
112
|
+
| Platform | Support |
|
|
113
|
+
|---|---|
|
|
114
|
+
| iOS 15+ | Full — `BGTaskScheduler` + optional APNs |
|
|
115
|
+
| Android | Unsupported in this iOS BGTaskScheduler bridge |
|
|
116
|
+
| Web / Electron | Unsupported fallback (`supported: false`) |
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { PluginListenerHandle } from "@capacitor/core";
|
|
2
|
+
/**
|
|
3
|
+
* Background task kinds the native plugin can wake the JS layer with.
|
|
4
|
+
*
|
|
5
|
+
* - `refresh` → iOS `BGAppRefreshTaskRequest` — short (~30s), network-OK
|
|
6
|
+
* foreground-equivalent wake. Used for the polling poke
|
|
7
|
+
* against `/api/internal/wake`.
|
|
8
|
+
* - `processing` → iOS `BGProcessingTaskRequest` — long-running (~minutes),
|
|
9
|
+
* runs while the device is charging and idle. Used for the
|
|
10
|
+
* local-LLM warmup pass that has no time pressure.
|
|
11
|
+
* - `remote-push` → silent APNs `content-available:1` push (gated on
|
|
12
|
+
* `ELIZA_APNS_ENABLED` in Info.plist). Mirrors the BG-task
|
|
13
|
+
* contract so the JS side can treat all three uniformly.
|
|
14
|
+
*/
|
|
15
|
+
export type ElizaTaskKind = "refresh" | "processing" | "remote-push";
|
|
16
|
+
export type ElizaTaskIdentifier = "ai.eliza.tasks.refresh" | "ai.eliza.tasks.processing";
|
|
17
|
+
/**
|
|
18
|
+
* Wake event delivered to JS listeners. Mirrors the `addEventListener("wake")`
|
|
19
|
+
* contract in `runners/eliza-tasks.js` so the BackgroundRunner runner and the
|
|
20
|
+
* BGTaskScheduler-driven runner share one client-side handler.
|
|
21
|
+
*/
|
|
22
|
+
export interface ElizaTasksWakeEvent {
|
|
23
|
+
kind: ElizaTaskKind;
|
|
24
|
+
identifier: ElizaTaskIdentifier | "ai.eliza.tasks.remote-push";
|
|
25
|
+
deadlineSec: number;
|
|
26
|
+
/** When the OS dispatched the wake (epoch ms). */
|
|
27
|
+
firedAtMs: number;
|
|
28
|
+
/**
|
|
29
|
+
* For `remote-push`, the raw `userInfo` from APNs (minus `aps`). For
|
|
30
|
+
* BGTaskScheduler wakes, an empty object — wake-time payload lives in the
|
|
31
|
+
* agent's loopback HTTP route.
|
|
32
|
+
*/
|
|
33
|
+
payload: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
export interface ElizaTasksScheduleOptions {
|
|
36
|
+
/**
|
|
37
|
+
* Earliest begin date for the next wake, expressed as seconds from now.
|
|
38
|
+
* `BGTaskScheduler` treats this as a hint — the OS controls the actual
|
|
39
|
+
* dispatch time. Must be ≥ 1.
|
|
40
|
+
*/
|
|
41
|
+
earliestBeginSec?: number;
|
|
42
|
+
/**
|
|
43
|
+
* If true, also enqueue the long-running BGProcessingTask. The processing
|
|
44
|
+
* variant only fires while the device is charging and idle.
|
|
45
|
+
*/
|
|
46
|
+
alsoProcessing?: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface ElizaTasksScheduleResult {
|
|
49
|
+
scheduled: boolean;
|
|
50
|
+
identifier: ElizaTaskIdentifier;
|
|
51
|
+
earliestBeginAtMs: number | null;
|
|
52
|
+
reason: string | null;
|
|
53
|
+
}
|
|
54
|
+
export interface ElizaTasksStatus {
|
|
55
|
+
supported: boolean;
|
|
56
|
+
platform: "ios" | "android" | "web";
|
|
57
|
+
refreshScheduled: boolean;
|
|
58
|
+
processingScheduled: boolean;
|
|
59
|
+
lastWakeFiredAtMs: number | null;
|
|
60
|
+
lastWakeKind: ElizaTaskKind | null;
|
|
61
|
+
reason: string | null;
|
|
62
|
+
}
|
|
63
|
+
export interface ElizaTasksPlugin {
|
|
64
|
+
/**
|
|
65
|
+
* Enqueue the next BG refresh wake. Idempotent — calling repeatedly
|
|
66
|
+
* replaces the pending request rather than stacking.
|
|
67
|
+
*/
|
|
68
|
+
scheduleNext(options?: ElizaTasksScheduleOptions): Promise<ElizaTasksScheduleResult>;
|
|
69
|
+
/**
|
|
70
|
+
* Snapshot of the plugin's view of BGTaskScheduler state. Used by the JS
|
|
71
|
+
* layer to decide whether to fall back to the BackgroundRunner repeat poll.
|
|
72
|
+
*/
|
|
73
|
+
getStatus(): Promise<ElizaTasksStatus>;
|
|
74
|
+
/**
|
|
75
|
+
* Cancel any pending refresh + processing requests. Used during the
|
|
76
|
+
* `disable mobile background tasks` flow.
|
|
77
|
+
*/
|
|
78
|
+
cancelAll(): Promise<{
|
|
79
|
+
cancelled: boolean;
|
|
80
|
+
}>;
|
|
81
|
+
addListener(eventName: "wake", listenerFunc: (event: ElizaTasksWakeEvent) => void): Promise<PluginListenerHandle>;
|
|
82
|
+
removeAllListeners(): Promise<void>;
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=definitions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"definitions.d.ts","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAE5D;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,YAAY,GAAG,aAAa,CAAC;AAErE,MAAM,MAAM,mBAAmB,GAC3B,wBAAwB,GACxB,2BAA2B,CAAC;AAEhC;;;;GAIG;AACH,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,aAAa,CAAC;IACpB,UAAU,EAAE,mBAAmB,GAAG,4BAA4B,CAAC;IAC/D,WAAW,EAAE,MAAM,CAAC;IACpB,kDAAkD;IAClD,SAAS,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,yBAAyB;IACxC;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,wBAAwB;IACvC,SAAS,EAAE,OAAO,CAAC;IACnB,UAAU,EAAE,mBAAmB,CAAC;IAChC,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,KAAK,GAAG,SAAS,GAAG,KAAK,CAAC;IACpC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,mBAAmB,EAAE,OAAO,CAAC;IAC7B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,YAAY,EAAE,aAAa,GAAG,IAAI,CAAC;IACnC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,YAAY,CACV,OAAO,CAAC,EAAE,yBAAyB,GAClC,OAAO,CAAC,wBAAwB,CAAC,CAAC;IAErC;;;OAGG;IACH,SAAS,IAAI,OAAO,CAAC,gBAAgB,CAAC,CAAC;IAEvC;;;OAGG;IACH,SAAS,IAAI,OAAO,CAAC;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IAE7C,WAAW,CACT,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,CAAC,KAAK,EAAE,mBAAmB,KAAK,IAAI,GACjD,OAAO,CAAC,oBAAoB,CAAC,CAAC;IAEjC,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,cAAc,eAAe,CAAC;AAI9B,eAAO,MAAM,UAAU,kBAErB,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { WebPlugin } from "@capacitor/core";
|
|
2
|
+
import type { ElizaTasksPlugin, ElizaTasksScheduleOptions, ElizaTasksScheduleResult, ElizaTasksStatus } from "./definitions";
|
|
3
|
+
/**
|
|
4
|
+
* Web fallback for the ElizaTasks plugin.
|
|
5
|
+
*
|
|
6
|
+
* Browsers have no `BGTaskScheduler` equivalent. The plugin resolves to a
|
|
7
|
+
* `supported: false` status so the runtime knows to fall back to the
|
|
8
|
+
* BackgroundRunner repeat poll (already configured in `capacitor.config.ts`).
|
|
9
|
+
*/
|
|
10
|
+
export declare class ElizaTasksWeb extends WebPlugin implements ElizaTasksPlugin {
|
|
11
|
+
scheduleNext(_options?: ElizaTasksScheduleOptions): Promise<ElizaTasksScheduleResult>;
|
|
12
|
+
getStatus(): Promise<ElizaTasksStatus>;
|
|
13
|
+
cancelAll(): Promise<{
|
|
14
|
+
cancelled: boolean;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
//# sourceMappingURL=web.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web.d.ts","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,KAAK,EACV,gBAAgB,EAChB,yBAAyB,EACzB,wBAAwB,EACxB,gBAAgB,EACjB,MAAM,eAAe,CAAC;AAEvB;;;;;;GAMG;AACH,qBAAa,aAAc,SAAQ,SAAU,YAAW,gBAAgB;IACtE,YAAY,CACV,QAAQ,CAAC,EAAE,yBAAyB,GACnC,OAAO,CAAC,wBAAwB,CAAC;IASpC,SAAS,IAAI,OAAO,CAAC,gBAAgB,CAAC;IAYtC,SAAS,IAAI,OAAO,CAAC;QAAE,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;CAG7C"}
|
package/dist/esm/web.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { WebPlugin } from "@capacitor/core";
|
|
2
|
+
/**
|
|
3
|
+
* Web fallback for the ElizaTasks plugin.
|
|
4
|
+
*
|
|
5
|
+
* Browsers have no `BGTaskScheduler` equivalent. The plugin resolves to a
|
|
6
|
+
* `supported: false` status so the runtime knows to fall back to the
|
|
7
|
+
* BackgroundRunner repeat poll (already configured in `capacitor.config.ts`).
|
|
8
|
+
*/
|
|
9
|
+
export class ElizaTasksWeb extends WebPlugin {
|
|
10
|
+
scheduleNext(_options) {
|
|
11
|
+
return Promise.resolve({
|
|
12
|
+
scheduled: false,
|
|
13
|
+
identifier: "ai.eliza.tasks.refresh",
|
|
14
|
+
earliestBeginAtMs: null,
|
|
15
|
+
reason: "BGTaskScheduler is iOS-only; web has no background wake path.",
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
getStatus() {
|
|
19
|
+
return Promise.resolve({
|
|
20
|
+
supported: false,
|
|
21
|
+
platform: "web",
|
|
22
|
+
refreshScheduled: false,
|
|
23
|
+
processingScheduled: false,
|
|
24
|
+
lastWakeFiredAtMs: null,
|
|
25
|
+
lastWakeKind: null,
|
|
26
|
+
reason: "BGTaskScheduler is iOS-only; web has no background wake path.",
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
cancelAll() {
|
|
30
|
+
return Promise.resolve({ cancelled: false });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"web.test.d.ts","sourceRoot":"","sources":["../../src/web.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { ElizaTasksWeb } from "./web";
|
|
3
|
+
describe("ElizaTasksWeb fallback", () => {
|
|
4
|
+
it("reports the browser as unsupported without throwing", async () => {
|
|
5
|
+
await expect(new ElizaTasksWeb().getStatus()).resolves.toEqual({
|
|
6
|
+
supported: false,
|
|
7
|
+
platform: "web",
|
|
8
|
+
refreshScheduled: false,
|
|
9
|
+
processingScheduled: false,
|
|
10
|
+
lastWakeFiredAtMs: null,
|
|
11
|
+
lastWakeKind: null,
|
|
12
|
+
reason: "BGTaskScheduler is iOS-only; web has no background wake path.",
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
it.each([
|
|
16
|
+
undefined,
|
|
17
|
+
{},
|
|
18
|
+
{ earliestBeginSec: Number.NaN, alsoProcessing: true },
|
|
19
|
+
{ earliestBeginSec: Number.POSITIVE_INFINITY, alsoProcessing: true },
|
|
20
|
+
{ earliestBeginSec: -1, alsoProcessing: true },
|
|
21
|
+
])("ignores schedule options on web %#", async (options) => {
|
|
22
|
+
await expect(new ElizaTasksWeb().scheduleNext(options)).resolves.toEqual({
|
|
23
|
+
scheduled: false,
|
|
24
|
+
identifier: "ai.eliza.tasks.refresh",
|
|
25
|
+
earliestBeginAtMs: null,
|
|
26
|
+
reason: "BGTaskScheduler is iOS-only; web has no background wake path.",
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
it("cancelAll reports that no web wake requests were cancelled", async () => {
|
|
30
|
+
await expect(new ElizaTasksWeb().cancelAll()).resolves.toEqual({
|
|
31
|
+
cancelled: false,
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var core = require('@capacitor/core');
|
|
4
|
+
|
|
5
|
+
const loadWeb = () => Promise.resolve().then(function () { return web; }).then((m) => new m.ElizaTasksWeb());
|
|
6
|
+
const ElizaTasks = core.registerPlugin("ElizaTasks", {
|
|
7
|
+
web: loadWeb,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Web fallback for the ElizaTasks plugin.
|
|
12
|
+
*
|
|
13
|
+
* Browsers have no `BGTaskScheduler` equivalent. The plugin resolves to a
|
|
14
|
+
* `supported: false` status so the runtime knows to fall back to the
|
|
15
|
+
* BackgroundRunner repeat poll (already configured in `capacitor.config.ts`).
|
|
16
|
+
*/
|
|
17
|
+
class ElizaTasksWeb extends core.WebPlugin {
|
|
18
|
+
scheduleNext(_options) {
|
|
19
|
+
return Promise.resolve({
|
|
20
|
+
scheduled: false,
|
|
21
|
+
identifier: "ai.eliza.tasks.refresh",
|
|
22
|
+
earliestBeginAtMs: null,
|
|
23
|
+
reason: "BGTaskScheduler is iOS-only; web has no background wake path.",
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
getStatus() {
|
|
27
|
+
return Promise.resolve({
|
|
28
|
+
supported: false,
|
|
29
|
+
platform: "web",
|
|
30
|
+
refreshScheduled: false,
|
|
31
|
+
processingScheduled: false,
|
|
32
|
+
lastWakeFiredAtMs: null,
|
|
33
|
+
lastWakeKind: null,
|
|
34
|
+
reason: "BGTaskScheduler is iOS-only; web has no background wake path.",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
cancelAll() {
|
|
38
|
+
return Promise.resolve({ cancelled: false });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
var web = /*#__PURE__*/Object.freeze({
|
|
43
|
+
__proto__: null,
|
|
44
|
+
ElizaTasksWeb: ElizaTasksWeb
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
exports.ElizaTasks = ElizaTasks;
|
|
48
|
+
//# sourceMappingURL=plugin.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from \"@capacitor/core\";\nexport * from \"./definitions\";\nconst loadWeb = () => import(\"./web\").then((m) => new m.ElizaTasksWeb());\nexport const ElizaTasks = registerPlugin(\"ElizaTasks\", {\n web: loadWeb,\n});\n","import { WebPlugin } from \"@capacitor/core\";\n/**\n * Web fallback for the ElizaTasks plugin.\n *\n * Browsers have no `BGTaskScheduler` equivalent. The plugin resolves to a\n * `supported: false` status so the runtime knows to fall back to the\n * BackgroundRunner repeat poll (already configured in `capacitor.config.ts`).\n */\nexport class ElizaTasksWeb extends WebPlugin {\n scheduleNext(_options) {\n return Promise.resolve({\n scheduled: false,\n identifier: \"ai.eliza.tasks.refresh\",\n earliestBeginAtMs: null,\n reason: \"BGTaskScheduler is iOS-only; web has no background wake path.\",\n });\n }\n getStatus() {\n return Promise.resolve({\n supported: false,\n platform: \"web\",\n refreshScheduled: false,\n processingScheduled: false,\n lastWakeFiredAtMs: null,\n lastWakeKind: null,\n reason: \"BGTaskScheduler is iOS-only; web has no background wake path.\",\n });\n }\n cancelAll() {\n return Promise.resolve({ cancelled: false });\n }\n}\n"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AAEA,MAAM,OAAO,GAAG,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,aAAa,EAAE,CAAC;AAC5D,MAAC,UAAU,GAAGA,mBAAc,CAAC,YAAY,EAAE;AACvD,IAAI,GAAG,EAAE,OAAO;AAChB,CAAC;;ACJD;AACA;AACA;AACA;AACA;AACA;AACA;AACO,MAAM,aAAa,SAASC,cAAS,CAAC;AAC7C,IAAI,YAAY,CAAC,QAAQ,EAAE;AAC3B,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC;AAC/B,YAAY,SAAS,EAAE,KAAK;AAC5B,YAAY,UAAU,EAAE,wBAAwB;AAChD,YAAY,iBAAiB,EAAE,IAAI;AACnC,YAAY,MAAM,EAAE,+DAA+D;AACnF,SAAS,CAAC;AACV,IAAI;AACJ,IAAI,SAAS,GAAG;AAChB,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC;AAC/B,YAAY,SAAS,EAAE,KAAK;AAC5B,YAAY,QAAQ,EAAE,KAAK;AAC3B,YAAY,gBAAgB,EAAE,KAAK;AACnC,YAAY,mBAAmB,EAAE,KAAK;AACtC,YAAY,iBAAiB,EAAE,IAAI;AACnC,YAAY,YAAY,EAAE,IAAI;AAC9B,YAAY,MAAM,EAAE,+DAA+D;AACnF,SAAS,CAAC;AACV,IAAI;AACJ,IAAI,SAAS,GAAG;AAChB,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;AACpD,IAAI;AACJ;;;;;;;;;"}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
var capacitorElizaTasks = (function (exports, core) {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const loadWeb = () => Promise.resolve().then(function () { return web; }).then((m) => new m.ElizaTasksWeb());
|
|
5
|
+
const ElizaTasks = core.registerPlugin("ElizaTasks", {
|
|
6
|
+
web: loadWeb,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Web fallback for the ElizaTasks plugin.
|
|
11
|
+
*
|
|
12
|
+
* Browsers have no `BGTaskScheduler` equivalent. The plugin resolves to a
|
|
13
|
+
* `supported: false` status so the runtime knows to fall back to the
|
|
14
|
+
* BackgroundRunner repeat poll (already configured in `capacitor.config.ts`).
|
|
15
|
+
*/
|
|
16
|
+
class ElizaTasksWeb extends core.WebPlugin {
|
|
17
|
+
scheduleNext(_options) {
|
|
18
|
+
return Promise.resolve({
|
|
19
|
+
scheduled: false,
|
|
20
|
+
identifier: "ai.eliza.tasks.refresh",
|
|
21
|
+
earliestBeginAtMs: null,
|
|
22
|
+
reason: "BGTaskScheduler is iOS-only; web has no background wake path.",
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
getStatus() {
|
|
26
|
+
return Promise.resolve({
|
|
27
|
+
supported: false,
|
|
28
|
+
platform: "web",
|
|
29
|
+
refreshScheduled: false,
|
|
30
|
+
processingScheduled: false,
|
|
31
|
+
lastWakeFiredAtMs: null,
|
|
32
|
+
lastWakeKind: null,
|
|
33
|
+
reason: "BGTaskScheduler is iOS-only; web has no background wake path.",
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
cancelAll() {
|
|
37
|
+
return Promise.resolve({ cancelled: false });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
var web = /*#__PURE__*/Object.freeze({
|
|
42
|
+
__proto__: null,
|
|
43
|
+
ElizaTasksWeb: ElizaTasksWeb
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
exports.ElizaTasks = ElizaTasks;
|
|
47
|
+
|
|
48
|
+
return exports;
|
|
49
|
+
|
|
50
|
+
})({}, capacitorExports);
|
|
51
|
+
//# sourceMappingURL=plugin.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from \"@capacitor/core\";\nexport * from \"./definitions\";\nconst loadWeb = () => import(\"./web\").then((m) => new m.ElizaTasksWeb());\nexport const ElizaTasks = registerPlugin(\"ElizaTasks\", {\n web: loadWeb,\n});\n","import { WebPlugin } from \"@capacitor/core\";\n/**\n * Web fallback for the ElizaTasks plugin.\n *\n * Browsers have no `BGTaskScheduler` equivalent. The plugin resolves to a\n * `supported: false` status so the runtime knows to fall back to the\n * BackgroundRunner repeat poll (already configured in `capacitor.config.ts`).\n */\nexport class ElizaTasksWeb extends WebPlugin {\n scheduleNext(_options) {\n return Promise.resolve({\n scheduled: false,\n identifier: \"ai.eliza.tasks.refresh\",\n earliestBeginAtMs: null,\n reason: \"BGTaskScheduler is iOS-only; web has no background wake path.\",\n });\n }\n getStatus() {\n return Promise.resolve({\n supported: false,\n platform: \"web\",\n refreshScheduled: false,\n processingScheduled: false,\n lastWakeFiredAtMs: null,\n lastWakeKind: null,\n reason: \"BGTaskScheduler is iOS-only; web has no background wake path.\",\n });\n }\n cancelAll() {\n return Promise.resolve({ cancelled: false });\n }\n}\n"],"names":["registerPlugin","WebPlugin"],"mappings":";;;IAEA,MAAM,OAAO,GAAG,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,aAAa,EAAE,CAAC;AAC5D,UAAC,UAAU,GAAGA,mBAAc,CAAC,YAAY,EAAE;IACvD,IAAI,GAAG,EAAE,OAAO;IAChB,CAAC;;ICJD;IACA;IACA;IACA;IACA;IACA;IACA;IACO,MAAM,aAAa,SAASC,cAAS,CAAC;IAC7C,IAAI,YAAY,CAAC,QAAQ,EAAE;IAC3B,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC;IAC/B,YAAY,SAAS,EAAE,KAAK;IAC5B,YAAY,UAAU,EAAE,wBAAwB;IAChD,YAAY,iBAAiB,EAAE,IAAI;IACnC,YAAY,MAAM,EAAE,+DAA+D;IACnF,SAAS,CAAC;IACV,IAAI;IACJ,IAAI,SAAS,GAAG;IAChB,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC;IAC/B,YAAY,SAAS,EAAE,KAAK;IAC5B,YAAY,QAAQ,EAAE,KAAK;IAC3B,YAAY,gBAAgB,EAAE,KAAK;IACnC,YAAY,mBAAmB,EAAE,KAAK;IACtC,YAAY,iBAAiB,EAAE,IAAI;IACnC,YAAY,YAAY,EAAE,IAAI;IAC9B,YAAY,MAAM,EAAE,+DAA+D;IACnF,SAAS,CAAC;IACV,IAAI;IACJ,IAAI,SAAS,GAAG;IAChB,QAAQ,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;IACpD,IAAI;IACJ;;;;;;;;;;;;;;;"}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import BackgroundTasks
|
|
2
|
+
import Capacitor
|
|
3
|
+
import Foundation
|
|
4
|
+
import UIKit
|
|
5
|
+
import UserNotifications
|
|
6
|
+
|
|
7
|
+
/// ElizaTasksPlugin — Capacitor bridge for iOS `BGTaskScheduler`.
|
|
8
|
+
///
|
|
9
|
+
/// Registers two namespaced identifiers in `BGTaskScheduler.shared.register`:
|
|
10
|
+
///
|
|
11
|
+
/// - `ai.eliza.tasks.refresh` → `BGAppRefreshTask` (≤ ~30s, network-OK)
|
|
12
|
+
/// - `ai.eliza.tasks.processing` → `BGProcessingTask` (long-running,
|
|
13
|
+
/// requires charger, no network)
|
|
14
|
+
///
|
|
15
|
+
/// On wake, the plugin emits a `wake` event to the JS layer carrying
|
|
16
|
+
/// `{kind, identifier, deadlineSec, firedAtMs, payload}`. The Wave 3D JS
|
|
17
|
+
/// runner under `runners/eliza-tasks.js` consumes the same event shape from
|
|
18
|
+
/// the `@capacitor/background-runner` repeat poll, so the JS-side handler is
|
|
19
|
+
/// shared between the two wake paths.
|
|
20
|
+
///
|
|
21
|
+
/// Silent-push wake (`remote-notification` UIBackgroundMode) is plumbed
|
|
22
|
+
/// through `AppDelegate.application:didReceiveRemoteNotification:` which
|
|
23
|
+
/// posts an `ElizaCompanionRemotePush` `NSNotification`. This plugin
|
|
24
|
+
/// observes that notification and forwards the payload as a third wake
|
|
25
|
+
/// `kind: "remote-push"` event. APNs registration is still gated on the
|
|
26
|
+
/// `ELIZA_APNS_ENABLED` Info.plist flag and defaults off.
|
|
27
|
+
///
|
|
28
|
+
/// JS-callable methods:
|
|
29
|
+
/// - `scheduleNext({ earliestBeginSec, alsoProcessing })`
|
|
30
|
+
/// - `getStatus()`
|
|
31
|
+
/// - `cancelAll()`
|
|
32
|
+
@objc(ElizaTasksPlugin)
|
|
33
|
+
public class ElizaTasksPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
34
|
+
public let identifier = "ElizaTasksPlugin"
|
|
35
|
+
public let jsName = "ElizaTasks"
|
|
36
|
+
public let pluginMethods: [CAPPluginMethod] = [
|
|
37
|
+
CAPPluginMethod(name: "scheduleNext", returnType: CAPPluginReturnPromise),
|
|
38
|
+
CAPPluginMethod(name: "getStatus", returnType: CAPPluginReturnPromise),
|
|
39
|
+
CAPPluginMethod(name: "cancelAll", returnType: CAPPluginReturnPromise),
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
// The identifiers MUST match the values declared in Info.plist's
|
|
43
|
+
// `BGTaskSchedulerPermittedIdentifiers` array. If they drift, `register`
|
|
44
|
+
// returns false and the OS refuses to dispatch tasks.
|
|
45
|
+
private static let refreshIdentifier = "ai.eliza.tasks.refresh"
|
|
46
|
+
private static let processingIdentifier = "ai.eliza.tasks.processing"
|
|
47
|
+
private static let remotePushIdentifier = "ai.eliza.tasks.remote-push"
|
|
48
|
+
|
|
49
|
+
// Default earliestBegin if the JS caller does not supply one. Apple's
|
|
50
|
+
// documented practical minimum is 15 minutes; we honor that as the floor.
|
|
51
|
+
private static let defaultEarliestBeginSec: Double = 15 * 60
|
|
52
|
+
|
|
53
|
+
// Apple's WWDC guidance: BGAppRefreshTask gets ~30s, BGProcessingTask
|
|
54
|
+
// gets minutes. The JS runner consumes deadlineSec to size its own
|
|
55
|
+
// hard-deadline race.
|
|
56
|
+
private static let refreshDeadlineSec: Double = 25
|
|
57
|
+
private static let processingDeadlineSec: Double = 120
|
|
58
|
+
|
|
59
|
+
// Persistence keys — used by `getStatus` so the JS side can observe wake
|
|
60
|
+
// events that landed before the webview was ready to receive listeners.
|
|
61
|
+
private static let lastWakeFiredAtKey = "ai.eliza.tasks.lastWakeFiredAtMs"
|
|
62
|
+
private static let lastWakeKindKey = "ai.eliza.tasks.lastWakeKind"
|
|
63
|
+
private static let refreshScheduledKey = "ai.eliza.tasks.refreshScheduled"
|
|
64
|
+
private static let processingScheduledKey = "ai.eliza.tasks.processingScheduled"
|
|
65
|
+
|
|
66
|
+
private var remotePushObserver: NSObjectProtocol?
|
|
67
|
+
|
|
68
|
+
public override func load() {
|
|
69
|
+
registerBackgroundTasks()
|
|
70
|
+
observeRemotePush()
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
deinit {
|
|
74
|
+
if let observer = remotePushObserver {
|
|
75
|
+
NotificationCenter.default.removeObserver(observer)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// MARK: - JS-callable surface
|
|
80
|
+
|
|
81
|
+
@objc func scheduleNext(_ call: CAPPluginCall) {
|
|
82
|
+
let earliestBeginSec = call.getDouble("earliestBeginSec")
|
|
83
|
+
?? Self.defaultEarliestBeginSec
|
|
84
|
+
let alsoProcessing = call.getBool("alsoProcessing") ?? false
|
|
85
|
+
|
|
86
|
+
// Floor at 1s — BGTaskScheduler returns an error for negative or
|
|
87
|
+
// zero values, and very small intervals are silently coerced anyway.
|
|
88
|
+
let normalizedBegin = max(1.0, earliestBeginSec)
|
|
89
|
+
let beginDate = Date(timeIntervalSinceNow: normalizedBegin)
|
|
90
|
+
|
|
91
|
+
let refreshResult = submitRefreshRequest(beginDate: beginDate)
|
|
92
|
+
if alsoProcessing {
|
|
93
|
+
_ = submitProcessingRequest(beginDate: beginDate)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
call.resolve([
|
|
97
|
+
"scheduled": refreshResult.scheduled,
|
|
98
|
+
"identifier": Self.refreshIdentifier,
|
|
99
|
+
"earliestBeginAtMs": refreshResult.scheduled
|
|
100
|
+
? Int64(beginDate.timeIntervalSince1970 * 1000)
|
|
101
|
+
: NSNull(),
|
|
102
|
+
"reason": refreshResult.reason as Any? ?? NSNull(),
|
|
103
|
+
])
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
@objc func getStatus(_ call: CAPPluginCall) {
|
|
107
|
+
let defaults = UserDefaults.standard
|
|
108
|
+
let lastWakeMs = defaults.object(forKey: Self.lastWakeFiredAtKey) as? Int64
|
|
109
|
+
let lastKind = defaults.string(forKey: Self.lastWakeKindKey)
|
|
110
|
+
|
|
111
|
+
call.resolve([
|
|
112
|
+
"supported": true,
|
|
113
|
+
"platform": "ios",
|
|
114
|
+
"refreshScheduled": defaults.bool(forKey: Self.refreshScheduledKey),
|
|
115
|
+
"processingScheduled": defaults.bool(forKey: Self.processingScheduledKey),
|
|
116
|
+
"lastWakeFiredAtMs": lastWakeMs.map { $0 as Any } ?? NSNull(),
|
|
117
|
+
"lastWakeKind": lastKind as Any? ?? NSNull(),
|
|
118
|
+
"reason": NSNull(),
|
|
119
|
+
])
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@objc func cancelAll(_ call: CAPPluginCall) {
|
|
123
|
+
BGTaskScheduler.shared.cancelAllTaskRequests()
|
|
124
|
+
let defaults = UserDefaults.standard
|
|
125
|
+
defaults.set(false, forKey: Self.refreshScheduledKey)
|
|
126
|
+
defaults.set(false, forKey: Self.processingScheduledKey)
|
|
127
|
+
call.resolve(["cancelled": true])
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// MARK: - BGTaskScheduler registration
|
|
131
|
+
|
|
132
|
+
/// Register the two BG task handlers. Must run before
|
|
133
|
+
/// `application:didFinishLaunchingWithOptions:` returns. CAPPlugin.load()
|
|
134
|
+
/// runs after `didFinishLaunching` but BGTaskScheduler tolerates this if
|
|
135
|
+
/// the identifier is in Info.plist's permitted list — the OS queues the
|
|
136
|
+
/// dispatch until a handler is registered. Confirmed on iOS 15+.
|
|
137
|
+
private func registerBackgroundTasks() {
|
|
138
|
+
let scheduler = BGTaskScheduler.shared
|
|
139
|
+
|
|
140
|
+
let refreshRegistered = scheduler.register(
|
|
141
|
+
forTaskWithIdentifier: Self.refreshIdentifier,
|
|
142
|
+
using: nil
|
|
143
|
+
) { [weak self] task in
|
|
144
|
+
guard let self = self else {
|
|
145
|
+
task.setTaskCompleted(success: false)
|
|
146
|
+
return
|
|
147
|
+
}
|
|
148
|
+
self.handleRefreshTask(task)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let processingRegistered = scheduler.register(
|
|
152
|
+
forTaskWithIdentifier: Self.processingIdentifier,
|
|
153
|
+
using: nil
|
|
154
|
+
) { [weak self] task in
|
|
155
|
+
guard let self = self else {
|
|
156
|
+
task.setTaskCompleted(success: false)
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
self.handleProcessingTask(task)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if !refreshRegistered {
|
|
163
|
+
NSLog(
|
|
164
|
+
"[ElizaTasksPlugin] BGTaskScheduler.register(%@) returned false — verify Info.plist BGTaskSchedulerPermittedIdentifiers",
|
|
165
|
+
Self.refreshIdentifier
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
if !processingRegistered {
|
|
169
|
+
NSLog(
|
|
170
|
+
"[ElizaTasksPlugin] BGTaskScheduler.register(%@) returned false — verify Info.plist BGTaskSchedulerPermittedIdentifiers",
|
|
171
|
+
Self.processingIdentifier
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// MARK: - Task handlers
|
|
177
|
+
|
|
178
|
+
private func handleRefreshTask(_ task: BGTask) {
|
|
179
|
+
// Always set an expiration handler before doing async work, otherwise
|
|
180
|
+
// iOS escalates the task termination to a force-kill.
|
|
181
|
+
task.expirationHandler = { [weak task] in
|
|
182
|
+
task?.setTaskCompleted(success: false)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
let firedAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
|
186
|
+
persistWake(kind: "refresh", firedAtMs: firedAtMs)
|
|
187
|
+
|
|
188
|
+
// BG refresh consumers are expected to re-enqueue the next wake before
|
|
189
|
+
// resolving — without this the chain dies after one fire.
|
|
190
|
+
let nextBegin = Date(timeIntervalSinceNow: Self.defaultEarliestBeginSec)
|
|
191
|
+
_ = submitRefreshRequest(beginDate: nextBegin)
|
|
192
|
+
|
|
193
|
+
emitWake(
|
|
194
|
+
kind: "refresh",
|
|
195
|
+
identifier: Self.refreshIdentifier,
|
|
196
|
+
deadlineSec: Self.refreshDeadlineSec,
|
|
197
|
+
firedAtMs: firedAtMs,
|
|
198
|
+
payload: [:]
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
// The JS handler runs in the webview, which may be backgrounded. We
|
|
202
|
+
// signal completion eagerly — the runner's HTTP poke is fire-and-forget
|
|
203
|
+
// from the OS's perspective; the agent's loopback `/api/internal/wake`
|
|
204
|
+
// route owns durability.
|
|
205
|
+
task.setTaskCompleted(success: true)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private func handleProcessingTask(_ task: BGTask) {
|
|
209
|
+
task.expirationHandler = { [weak task] in
|
|
210
|
+
task?.setTaskCompleted(success: false)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
let firedAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
|
214
|
+
persistWake(kind: "processing", firedAtMs: firedAtMs)
|
|
215
|
+
|
|
216
|
+
emitWake(
|
|
217
|
+
kind: "processing",
|
|
218
|
+
identifier: Self.processingIdentifier,
|
|
219
|
+
deadlineSec: Self.processingDeadlineSec,
|
|
220
|
+
firedAtMs: firedAtMs,
|
|
221
|
+
payload: [:]
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
// Processing tasks are not auto-rescheduled — the JS layer requests
|
|
225
|
+
// the next processing wake explicitly when a warmup window is needed.
|
|
226
|
+
task.setTaskCompleted(success: true)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// MARK: - Submit helpers
|
|
230
|
+
|
|
231
|
+
private struct SubmitResult {
|
|
232
|
+
let scheduled: Bool
|
|
233
|
+
let reason: String?
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private func submitRefreshRequest(beginDate: Date) -> SubmitResult {
|
|
237
|
+
let request = BGAppRefreshTaskRequest(identifier: Self.refreshIdentifier)
|
|
238
|
+
request.earliestBeginDate = beginDate
|
|
239
|
+
do {
|
|
240
|
+
try BGTaskScheduler.shared.submit(request)
|
|
241
|
+
UserDefaults.standard.set(true, forKey: Self.refreshScheduledKey)
|
|
242
|
+
return SubmitResult(scheduled: true, reason: nil)
|
|
243
|
+
} catch {
|
|
244
|
+
UserDefaults.standard.set(false, forKey: Self.refreshScheduledKey)
|
|
245
|
+
let message = "BGTaskScheduler.submit(refresh) failed: \(error.localizedDescription)"
|
|
246
|
+
NSLog("[ElizaTasksPlugin] %@", message)
|
|
247
|
+
return SubmitResult(scheduled: false, reason: message)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
private func submitProcessingRequest(beginDate: Date) -> SubmitResult {
|
|
252
|
+
let request = BGProcessingTaskRequest(identifier: Self.processingIdentifier)
|
|
253
|
+
request.earliestBeginDate = beginDate
|
|
254
|
+
// Eliza's processing task is the local-LLM warmup pass. Real-world
|
|
255
|
+
// semantics: only run while plugged in, no network needed (the agent
|
|
256
|
+
// is reachable on loopback inside the app process).
|
|
257
|
+
request.requiresExternalPower = true
|
|
258
|
+
request.requiresNetworkConnectivity = false
|
|
259
|
+
do {
|
|
260
|
+
try BGTaskScheduler.shared.submit(request)
|
|
261
|
+
UserDefaults.standard.set(true, forKey: Self.processingScheduledKey)
|
|
262
|
+
return SubmitResult(scheduled: true, reason: nil)
|
|
263
|
+
} catch {
|
|
264
|
+
UserDefaults.standard.set(false, forKey: Self.processingScheduledKey)
|
|
265
|
+
let message = "BGTaskScheduler.submit(processing) failed: \(error.localizedDescription)"
|
|
266
|
+
NSLog("[ElizaTasksPlugin] %@", message)
|
|
267
|
+
return SubmitResult(scheduled: false, reason: message)
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// MARK: - Remote push plumbing
|
|
272
|
+
|
|
273
|
+
/// AppDelegate's `application:didReceiveRemoteNotification:` posts an
|
|
274
|
+
/// `ElizaCompanionRemotePush` notification with the userInfo dictionary
|
|
275
|
+
/// in `object`. We surface that to JS as a third wake kind so the same
|
|
276
|
+
/// `wake` event handler can drain it. Gated on the operator flipping
|
|
277
|
+
/// `ELIZA_APNS_ENABLED=1` in Info.plist — silent until then.
|
|
278
|
+
private func observeRemotePush() {
|
|
279
|
+
remotePushObserver = NotificationCenter.default.addObserver(
|
|
280
|
+
forName: Notification.Name("ElizaCompanionRemotePush"),
|
|
281
|
+
object: nil,
|
|
282
|
+
queue: .main
|
|
283
|
+
) { [weak self] notification in
|
|
284
|
+
guard let self = self else { return }
|
|
285
|
+
let userInfo = (notification.object as? [AnyHashable: Any]) ?? [:]
|
|
286
|
+
var payload: [String: Any] = [:]
|
|
287
|
+
for (key, value) in userInfo {
|
|
288
|
+
guard let stringKey = key as? String, stringKey != "aps" else { continue }
|
|
289
|
+
payload[stringKey] = value
|
|
290
|
+
}
|
|
291
|
+
let firedAtMs = Int64(Date().timeIntervalSince1970 * 1000)
|
|
292
|
+
self.persistWake(kind: "remote-push", firedAtMs: firedAtMs)
|
|
293
|
+
self.emitWake(
|
|
294
|
+
kind: "remote-push",
|
|
295
|
+
identifier: Self.remotePushIdentifier,
|
|
296
|
+
deadlineSec: Self.refreshDeadlineSec,
|
|
297
|
+
firedAtMs: firedAtMs,
|
|
298
|
+
payload: payload
|
|
299
|
+
)
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// MARK: - Event emission
|
|
304
|
+
|
|
305
|
+
private func emitWake(
|
|
306
|
+
kind: String,
|
|
307
|
+
identifier: String,
|
|
308
|
+
deadlineSec: Double,
|
|
309
|
+
firedAtMs: Int64,
|
|
310
|
+
payload: [String: Any]
|
|
311
|
+
) {
|
|
312
|
+
notifyListeners("wake", data: [
|
|
313
|
+
"kind": kind,
|
|
314
|
+
"identifier": identifier,
|
|
315
|
+
"deadlineSec": deadlineSec,
|
|
316
|
+
"firedAtMs": firedAtMs,
|
|
317
|
+
"payload": payload,
|
|
318
|
+
])
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
private func persistWake(kind: String, firedAtMs: Int64) {
|
|
322
|
+
let defaults = UserDefaults.standard
|
|
323
|
+
defaults.set(firedAtMs, forKey: Self.lastWakeFiredAtKey)
|
|
324
|
+
defaults.set(kind, forKey: Self.lastWakeKindKey)
|
|
325
|
+
}
|
|
326
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@elizaos/capacitor-eliza-tasks",
|
|
3
|
+
"version": "2.0.3-beta.2",
|
|
4
|
+
"description": "Bridges iOS BGTaskScheduler (BGAppRefreshTask + BGProcessingTask) wake events into the Eliza Capacitor runtime.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"background",
|
|
7
|
+
"bgtaskscheduler",
|
|
8
|
+
"ios",
|
|
9
|
+
"wake",
|
|
10
|
+
"eliza"
|
|
11
|
+
],
|
|
12
|
+
"main": "./dist/plugin.cjs.js",
|
|
13
|
+
"module": "./dist/esm/index.js",
|
|
14
|
+
"types": "./dist/esm/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/esm/index.d.ts",
|
|
18
|
+
"bun": "./src/index.ts",
|
|
19
|
+
"development": "./src/index.ts",
|
|
20
|
+
"import": "./dist/esm/index.js",
|
|
21
|
+
"require": "./dist/plugin.cjs.js"
|
|
22
|
+
},
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"unpkg": "dist/plugin.js",
|
|
26
|
+
"files": [
|
|
27
|
+
"android/src/main/",
|
|
28
|
+
"android/build.gradle",
|
|
29
|
+
"dist/",
|
|
30
|
+
"ios/Sources/",
|
|
31
|
+
"*.podspec",
|
|
32
|
+
"dist"
|
|
33
|
+
],
|
|
34
|
+
"author": "elizaOS",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/elizaOS/eliza"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "node ../../packages/scripts/with-package-build-lock.mjs plugins/plugin-native-eliza-tasks -- bun run build:unlocked",
|
|
42
|
+
"clean": "node ../../packages/scripts/rm-path-recursive.mjs dist",
|
|
43
|
+
"prepublishOnly": "bun run build",
|
|
44
|
+
"test": "vitest run --passWithNoTests",
|
|
45
|
+
"watch": "tsc --watch",
|
|
46
|
+
"build:unlocked": "bun run clean && bunx tsc -p tsconfig.json && bunx rollup -c rollup.config.mjs"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@capacitor/core": "^8.3.1",
|
|
50
|
+
"@rollup/plugin-node-resolve": "^16.0.0",
|
|
51
|
+
"rollup": "^4.60.2",
|
|
52
|
+
"typescript": "^6.0.3",
|
|
53
|
+
"vitest": "^4.0.17"
|
|
54
|
+
},
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"@capacitor/core": "^8.3.1"
|
|
57
|
+
},
|
|
58
|
+
"publishConfig": {
|
|
59
|
+
"access": "public"
|
|
60
|
+
},
|
|
61
|
+
"capacitor": {
|
|
62
|
+
"ios": {
|
|
63
|
+
"src": "ios",
|
|
64
|
+
"podName": "ElizaosCapacitorElizaTasks"
|
|
65
|
+
},
|
|
66
|
+
"android": {
|
|
67
|
+
"src": "android"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"elizaos": {
|
|
71
|
+
"platforms": [
|
|
72
|
+
"browser",
|
|
73
|
+
"node"
|
|
74
|
+
],
|
|
75
|
+
"runtime": "both",
|
|
76
|
+
"platformDetails": {
|
|
77
|
+
"browser": "Unsupported web fallback; BGTaskScheduler is iOS-only.",
|
|
78
|
+
"node": "No desktop equivalent; runtime ignores this plugin off-mobile.",
|
|
79
|
+
"ios": true,
|
|
80
|
+
"android": false
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"gitHead": "82fe0f44215954c2417328203f5bd6510985c1fc"
|
|
84
|
+
}
|