jw-automator 1.0.2 โ 3.0.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.
- package/CHANGELOG.md +76 -0
- package/README.md +452 -1
- package/docs/ARCHITECTURE.md +342 -0
- package/docs/MIGRATION.md +407 -0
- package/docs/QUICKSTART.md +350 -0
- package/examples/basic-example.js +135 -0
- package/examples/hello-world.js +40 -0
- package/examples/iot-sensor-example.js +149 -0
- package/index.js +7 -0
- package/package.json +53 -19
- package/src/Automator.js +429 -0
- package/src/core/CoreEngine.js +210 -0
- package/src/core/RecurrenceEngine.js +237 -0
- package/src/host/SchedulerHost.js +174 -0
- package/src/storage/FileStorage.js +59 -0
- package/src/storage/MemoryStorage.js +27 -0
- package/.actions.json +0 -1
- package/.jshintrc +0 -16
- package/.vscode/settings.json +0 -6
- package/LICENSE +0 -674
- package/automator.js +0 -694
- package/demo.js +0 -76
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to jw-automator will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [3.0.0] - 2025-11-17
|
|
9
|
+
|
|
10
|
+
### Added (v3 Complete Rewrite)
|
|
11
|
+
|
|
12
|
+
- **Core Engine**: Pure deterministic `step()` function for scheduling
|
|
13
|
+
- **1-Second Precision**: Fixed 1-second tick interval with zero drift
|
|
14
|
+
- **Local-Time Semantics**: All recurrence rules operate in local wall-clock time
|
|
15
|
+
- **DST Handling**: Configurable DST policies (`once`/`twice`) for fall-back scenarios
|
|
16
|
+
- **Offline Catch-Up**: Buffered/unBuffered execution semantics for resilience
|
|
17
|
+
- **Recurrence Types**:
|
|
18
|
+
- `second`, `minute`, `hour`
|
|
19
|
+
- `day`, `weekday`, `weekend`, `week`
|
|
20
|
+
- `month`, `year`
|
|
21
|
+
- **Simulation API**: `getActionsInRange()` for future schedule preview
|
|
22
|
+
- **Pluggable Storage**: File-based and memory-based storage adapters
|
|
23
|
+
- **Custom Storage Interface**: Support for custom persistence backends
|
|
24
|
+
- **Event System**:
|
|
25
|
+
- `ready` - Scheduler started
|
|
26
|
+
- `action` - Action executed
|
|
27
|
+
- `update` - Actions added/updated/removed
|
|
28
|
+
- `error` - Error events
|
|
29
|
+
- `debug` - Debug information
|
|
30
|
+
- **Action Management**:
|
|
31
|
+
- `addAction()` - Add new scheduled actions
|
|
32
|
+
- `updateActionByID()` - Update existing actions
|
|
33
|
+
- `removeActionByID()` - Remove by ID
|
|
34
|
+
- `removeActionByName()` - Remove by name
|
|
35
|
+
- `getActions()` - Get all actions
|
|
36
|
+
- `getActionsByName()` - Query by name
|
|
37
|
+
- `describeAction()` - Human-readable description
|
|
38
|
+
- **Meta-Actions**: Actions can create/modify other actions
|
|
39
|
+
- **Auto-Save**: Configurable auto-save with custom intervals
|
|
40
|
+
- **Safety Guards**:
|
|
41
|
+
- Maximum iteration limits to prevent infinite loops
|
|
42
|
+
- Monotonic time progression guarantees
|
|
43
|
+
- Bounded per-tick execution
|
|
44
|
+
- **Comprehensive Tests**: Full test suite with Jest
|
|
45
|
+
- **Examples**: Basic and IoT sensor examples included
|
|
46
|
+
|
|
47
|
+
### Changed from v2
|
|
48
|
+
|
|
49
|
+
- Complete clean-room rewrite
|
|
50
|
+
- Improved DST handling with explicit policies
|
|
51
|
+
- Better separation of action spec vs. state
|
|
52
|
+
- Deterministic core suitable for testing and simulation
|
|
53
|
+
- More predictable catch-up behavior
|
|
54
|
+
- Enhanced error handling and reporting
|
|
55
|
+
- Improved API ergonomics
|
|
56
|
+
|
|
57
|
+
### Technical Improvements
|
|
58
|
+
|
|
59
|
+
- Pure functional core engine
|
|
60
|
+
- Environment-agnostic architecture
|
|
61
|
+
- Reduced edge-case bugs
|
|
62
|
+
- Better maintainability
|
|
63
|
+
- Comprehensive documentation
|
|
64
|
+
- Type-safe action specifications
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## [2.x] - Legacy
|
|
69
|
+
|
|
70
|
+
**Production version - widely deployed**
|
|
71
|
+
|
|
72
|
+
Version 2.x was the stable, production release of jw-automator that was actively used in home automation, IoT projects, and personal servers. Available in git history.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
For upgrade guidance from v2 to v3, see the migration guide in the documentation.
|
package/README.md
CHANGED
|
@@ -1 +1,452 @@
|
|
|
1
|
-
#
|
|
1
|
+
# ๐ **jw-automator v3**
|
|
2
|
+
|
|
3
|
+
### A resilient, local-time, 1-second precision automation scheduler for Node.js
|
|
4
|
+
|
|
5
|
+
**Human-friendly recurrence rules. Offline catch-up. DST-safe. Predictable. Extensible.**
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## โญ๏ธ Overview
|
|
10
|
+
|
|
11
|
+
**jw-automator** is a robust automation engine designed for small devices, home automation hubs, personal servers, and Node.js environments where **correctness, resilience, and local-time behavior** matter more than millisecond precision.
|
|
12
|
+
|
|
13
|
+
Where traditional cron falls short โ missed executions, poor DST handling, limited recurrence, lack of catch-up semantics โ **jw-automator** provides a predictable, human-centric scheduling model:
|
|
14
|
+
|
|
15
|
+
* **1-second granularity** with zero drift
|
|
16
|
+
* **Local calendar semantics** (weekday/weekend, monthly, yearly)
|
|
17
|
+
* **Configurable DST policies** (fall-back once/twice)
|
|
18
|
+
* **Offline resiliency & catch-up logic**
|
|
19
|
+
* **Buffered/unBuffered execution policies**
|
|
20
|
+
* **Rich introspection and event lifecycle**
|
|
21
|
+
* **Meta-actions** that can dynamically create/update other actions
|
|
22
|
+
* **Pluggable persistence** (file, memory, custom storage)
|
|
23
|
+
* **Deterministic step engine** suitable for simulation/testing
|
|
24
|
+
|
|
25
|
+
This makes jw-automator ideal for:
|
|
26
|
+
|
|
27
|
+
* Small Raspberry Pi home automation hubs
|
|
28
|
+
* IoT applications
|
|
29
|
+
* Sensor sampling / periodic readings
|
|
30
|
+
* Daily/weekly routines
|
|
31
|
+
* "Smart home" orchestrations
|
|
32
|
+
* Systems that must *survive restarts, reboots, offline gaps, and DST transitions*
|
|
33
|
+
|
|
34
|
+
jw-automator v3 is a **clean-room re-architecture** of the original library, keeping its best ideas while formalizing its semantics, improving correctness, and providing a crisp developer experience.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## ๐ Quick Start
|
|
39
|
+
|
|
40
|
+
### Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
npm install jw-automator
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Basic Usage
|
|
47
|
+
|
|
48
|
+
```js
|
|
49
|
+
const Automator = require('jw-automator');
|
|
50
|
+
|
|
51
|
+
// Create an automator with file-based persistence
|
|
52
|
+
const automator = new Automator({
|
|
53
|
+
storage: Automator.storage.file('./actions.json')
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Register a command function
|
|
57
|
+
automator.addFunction('turnLightOn', function(payload) {
|
|
58
|
+
console.log('Turning light on');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Add an action
|
|
62
|
+
automator.addAction({
|
|
63
|
+
name: 'Morning Lights',
|
|
64
|
+
cmd: 'turnLightOn',
|
|
65
|
+
date: new Date('2025-05-01T07:00:00'),
|
|
66
|
+
payload: null,
|
|
67
|
+
unBuffered: false,
|
|
68
|
+
repeat: {
|
|
69
|
+
type: 'day',
|
|
70
|
+
interval: 1,
|
|
71
|
+
limit: null,
|
|
72
|
+
endDate: null,
|
|
73
|
+
dstPolicy: 'once'
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// Start the scheduler
|
|
78
|
+
automator.start();
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## ๐ฅ Features
|
|
84
|
+
|
|
85
|
+
### 1. **True 1-Second Precision**
|
|
86
|
+
|
|
87
|
+
* Scheduler tick interval is fixed at **1 second**.
|
|
88
|
+
* Execution times are aligned to the nearest whole second.
|
|
89
|
+
* No promise of sub-second timing (by design).
|
|
90
|
+
* Ideal for low-power hardware prone to event-loop delays.
|
|
91
|
+
|
|
92
|
+
> **Why?**
|
|
93
|
+
> A scheduler that *promises less* is dramatically more reliable.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
### 2. **Human-Friendly Recurrence Rules**
|
|
98
|
+
|
|
99
|
+
Each action can specify a recurrence like:
|
|
100
|
+
|
|
101
|
+
```js
|
|
102
|
+
repeat: {
|
|
103
|
+
type: 'weekday', // or: second, minute, hour, day, week,
|
|
104
|
+
// month, year, weekend
|
|
105
|
+
interval: 1, // every N occurrences
|
|
106
|
+
limit: null, // optional max count
|
|
107
|
+
endDate: null, // optional cutoff date
|
|
108
|
+
dstPolicy: 'once', // or 'twice'
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
Examples:
|
|
113
|
+
|
|
114
|
+
* Every day at 7:00 AM
|
|
115
|
+
* Every 15 minutes
|
|
116
|
+
* Every weekend at 10:00
|
|
117
|
+
* Every month on the 1st
|
|
118
|
+
* Every weekday at market open
|
|
119
|
+
* Once per second for 5 minutes (limit-based)
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
### 3. **Local-Time First, DST-Aware**
|
|
124
|
+
|
|
125
|
+
jw-automator's recurrence rules operate in **local wall-clock time**, not UTC.
|
|
126
|
+
|
|
127
|
+
This means:
|
|
128
|
+
|
|
129
|
+
* "7:00 AM" always means **local** 7:00 AM.
|
|
130
|
+
* Weekdays/weekends follow the user's locale.
|
|
131
|
+
* DST transitions are **explicit and predictable**:
|
|
132
|
+
|
|
133
|
+
* **Spring forward:** missing hour handled via buffered/unBuffered rules
|
|
134
|
+
* **Fall back:** user chooses `dstPolicy: 'once' | 'twice'`
|
|
135
|
+
|
|
136
|
+
This avoids cron's silent-but-surprising behaviors.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
### 4. **Resilient Offline Catch-Up**
|
|
141
|
+
|
|
142
|
+
If the device is offline or delayed (e.g., blocked by CPU load):
|
|
143
|
+
|
|
144
|
+
```js
|
|
145
|
+
unBuffered: false // default: catch up missed executions
|
|
146
|
+
unBuffered: true // skip missed executions
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
For example:
|
|
150
|
+
|
|
151
|
+
* A job scheduled at 09:00 will still run when the device restarts at 10:00 (if buffered).
|
|
152
|
+
* A sequence of per-second readings will "compress" naturally after a delay.
|
|
153
|
+
|
|
154
|
+
This feature is ideal for:
|
|
155
|
+
|
|
156
|
+
* Home automation logic ("turn heater off at 9 even if offline")
|
|
157
|
+
* Sensor sampling
|
|
158
|
+
* Data collection pipelines
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
### 5. **Deterministic "Step Engine"**
|
|
163
|
+
|
|
164
|
+
The heart of jw-automator is a pure scheduling primitive:
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
step(state, lastTick, now) โ { newState, events }
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
This powers:
|
|
171
|
+
|
|
172
|
+
* Real-time ticking
|
|
173
|
+
* Offline catch-up
|
|
174
|
+
* Future schedule simulation
|
|
175
|
+
* Testing
|
|
176
|
+
* Meta-scheduling (actions that schedule other actions)
|
|
177
|
+
|
|
178
|
+
Because `step` is deterministic, you can:
|
|
179
|
+
|
|
180
|
+
* Test schedules without time passing
|
|
181
|
+
* Generate "what would happen tomorrow"
|
|
182
|
+
* Debug recurrence rules
|
|
183
|
+
* Build custom visual schedulers
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
### 6. **Meta-Actions (Actions that Create Actions)**
|
|
188
|
+
|
|
189
|
+
jw-automator treats actions as **data**, enabling higher-order patterns:
|
|
190
|
+
|
|
191
|
+
* A daily 7:00 AM action can spawn a sequence of 60 one-per-second actions.
|
|
192
|
+
* A monthly billing action can create daily reminder actions.
|
|
193
|
+
* A multi-step animation (e.g., dimming a light) can create timed sub-actions.
|
|
194
|
+
|
|
195
|
+
Actions have a `repeat.count` that can be pre-set or manipulated intentionally.
|
|
196
|
+
|
|
197
|
+
This makes jw-automator more like a *mini automation runtime* than just a cron clone.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## ๐ API Reference
|
|
202
|
+
|
|
203
|
+
### Constructor
|
|
204
|
+
|
|
205
|
+
```js
|
|
206
|
+
new Automator(options)
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Options:
|
|
210
|
+
* `storage` - Storage adapter (default: memory)
|
|
211
|
+
* `autoSave` - Auto-save state (default: true)
|
|
212
|
+
* `saveInterval` - Save interval in ms (default: 5000)
|
|
213
|
+
|
|
214
|
+
### Methods
|
|
215
|
+
|
|
216
|
+
#### `start()`
|
|
217
|
+
Start the scheduler.
|
|
218
|
+
|
|
219
|
+
#### `stop()`
|
|
220
|
+
Stop the scheduler and save state.
|
|
221
|
+
|
|
222
|
+
#### `addFunction(name, fn)`
|
|
223
|
+
Register a command function.
|
|
224
|
+
|
|
225
|
+
```js
|
|
226
|
+
automator.addFunction('myCommand', function(payload, event) {
|
|
227
|
+
console.log('Executing command with payload:', payload);
|
|
228
|
+
});
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
#### `addAction(actionSpec)`
|
|
232
|
+
Add a new action. Returns the action ID.
|
|
233
|
+
|
|
234
|
+
```js
|
|
235
|
+
const id = automator.addAction({
|
|
236
|
+
name: 'My Action',
|
|
237
|
+
cmd: 'myCommand',
|
|
238
|
+
date: new Date('2025-05-01T10:00:00'),
|
|
239
|
+
payload: { data: 'value' },
|
|
240
|
+
unBuffered: false,
|
|
241
|
+
repeat: {
|
|
242
|
+
type: 'hour',
|
|
243
|
+
interval: 2,
|
|
244
|
+
limit: 10,
|
|
245
|
+
dstPolicy: 'once'
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
#### `updateActionByID(id, updates)`
|
|
251
|
+
Update an existing action.
|
|
252
|
+
|
|
253
|
+
```js
|
|
254
|
+
automator.updateActionByID(1, {
|
|
255
|
+
name: 'Updated Name',
|
|
256
|
+
repeat: { type: 'day', interval: 1 }
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
#### `removeActionByID(id)`
|
|
261
|
+
Remove an action by ID.
|
|
262
|
+
|
|
263
|
+
#### `removeActionByName(name)`
|
|
264
|
+
Remove all actions with the given name.
|
|
265
|
+
|
|
266
|
+
#### `getActions()`
|
|
267
|
+
Get all actions (deep copy).
|
|
268
|
+
|
|
269
|
+
#### `getActionsByName(name)`
|
|
270
|
+
Get actions by name.
|
|
271
|
+
|
|
272
|
+
#### `getActionByID(id)`
|
|
273
|
+
Get a specific action by ID.
|
|
274
|
+
|
|
275
|
+
#### `getActionsInRange(startDate, endDate, callback)`
|
|
276
|
+
Simulate actions in a time range.
|
|
277
|
+
|
|
278
|
+
```js
|
|
279
|
+
const events = automator.getActionsInRange(
|
|
280
|
+
new Date('2025-05-01'),
|
|
281
|
+
new Date('2025-05-07')
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
console.log(events); // Array of scheduled events
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
#### `describeAction(id)`
|
|
288
|
+
Get a human-readable description of an action.
|
|
289
|
+
|
|
290
|
+
### Events
|
|
291
|
+
|
|
292
|
+
Listen to events using `automator.on(event, callback)`:
|
|
293
|
+
|
|
294
|
+
* `ready` - Scheduler started
|
|
295
|
+
* `action` - Action executed
|
|
296
|
+
* `update` - Action added/updated/removed
|
|
297
|
+
* `error` - Error occurred
|
|
298
|
+
* `debug` - Debug information
|
|
299
|
+
|
|
300
|
+
```js
|
|
301
|
+
automator.on('action', (event) => {
|
|
302
|
+
console.log('Action executed:', event.name);
|
|
303
|
+
console.log('Scheduled:', event.scheduledTime);
|
|
304
|
+
console.log('Actual:', event.actualTime);
|
|
305
|
+
});
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Storage Adapters
|
|
309
|
+
|
|
310
|
+
#### File Storage
|
|
311
|
+
|
|
312
|
+
```js
|
|
313
|
+
const automator = new Automator({
|
|
314
|
+
storage: Automator.storage.file('./actions.json')
|
|
315
|
+
});
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
#### Memory Storage
|
|
319
|
+
|
|
320
|
+
```js
|
|
321
|
+
const automator = new Automator({
|
|
322
|
+
storage: Automator.storage.memory()
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
#### Custom Storage
|
|
327
|
+
|
|
328
|
+
```js
|
|
329
|
+
const automator = new Automator({
|
|
330
|
+
storage: {
|
|
331
|
+
load: function() {
|
|
332
|
+
// Return { actions: [...] }
|
|
333
|
+
},
|
|
334
|
+
save: function(state) {
|
|
335
|
+
// Save state
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
---
|
|
342
|
+
|
|
343
|
+
## ๐ Example: Sensor Reading Every Second
|
|
344
|
+
|
|
345
|
+
```js
|
|
346
|
+
automator.addAction({
|
|
347
|
+
name: 'TempSensor',
|
|
348
|
+
cmd: 'readTemp',
|
|
349
|
+
date: null, // run immediately
|
|
350
|
+
payload: null,
|
|
351
|
+
unBuffered: false, // catch up if delayed
|
|
352
|
+
repeat: {
|
|
353
|
+
type: 'second',
|
|
354
|
+
interval: 1
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
If the system stalls:
|
|
360
|
+
|
|
361
|
+
* At 00:00:00 โ reading #1
|
|
362
|
+
* Heavy load โ no ticks for 5 seconds
|
|
363
|
+
* At 00:00:06 โ automator triggers readings #2โ#6, advancing schedule
|
|
364
|
+
|
|
365
|
+
Your "60 readings per minute" pattern is preserved logically.
|
|
366
|
+
|
|
367
|
+
---
|
|
368
|
+
|
|
369
|
+
## ๐ฐ DST Behavior Examples
|
|
370
|
+
|
|
371
|
+
### Fall Back (Repeated Hour)
|
|
372
|
+
|
|
373
|
+
07:30 happens twice:
|
|
374
|
+
|
|
375
|
+
```
|
|
376
|
+
1) 07:30 (DST)
|
|
377
|
+
2) 07:30 (Standard)
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
User chooses:
|
|
381
|
+
|
|
382
|
+
* `dstPolicy: 'twice'` โ run both
|
|
383
|
+
* `dstPolicy: 'once'` โ run only the first instance
|
|
384
|
+
|
|
385
|
+
### Spring Forward (Missing Hour)
|
|
386
|
+
|
|
387
|
+
02:30 does not exist.
|
|
388
|
+
|
|
389
|
+
* Buffered โ run as soon as possible after the jump
|
|
390
|
+
* Unbuffered โ skip silently
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## ๐งช Testing
|
|
395
|
+
|
|
396
|
+
```bash
|
|
397
|
+
npm test
|
|
398
|
+
npm run test:coverage
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
---
|
|
402
|
+
|
|
403
|
+
## ๐ฆ Action Specification
|
|
404
|
+
|
|
405
|
+
### Top-level action fields:
|
|
406
|
+
|
|
407
|
+
| Field | Description |
|
|
408
|
+
| ------------ | ------------------------------------------------- |
|
|
409
|
+
| `id` | Unique internal identifier (auto-generated) |
|
|
410
|
+
| `name` | User label (optional) |
|
|
411
|
+
| `cmd` | Name of registered function to execute |
|
|
412
|
+
| `payload` | Data passed to the command |
|
|
413
|
+
| `date` | Next scheduled run time (local `Date`) |
|
|
414
|
+
| `unBuffered` | Skip missed events (`true`) or catch up (`false`) |
|
|
415
|
+
|
|
416
|
+
### Repeat block:
|
|
417
|
+
|
|
418
|
+
| Field | Description |
|
|
419
|
+
| ----------- | --------------------------------- |
|
|
420
|
+
| `type` | Recurrence unit |
|
|
421
|
+
| `interval` | Nth occurrence |
|
|
422
|
+
| `limit` | Number of times to run, or `null` |
|
|
423
|
+
| `endDate` | Max date, or `null` |
|
|
424
|
+
| `count` | Execution counter (internal) |
|
|
425
|
+
| `dstPolicy` | `'once'` or `'twice'` |
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
## ๐ฏ Project Goals (v3)
|
|
430
|
+
|
|
431
|
+
* Deterministic behavior
|
|
432
|
+
* Rock-solid DST handling
|
|
433
|
+
* Predictable local-time recurrence
|
|
434
|
+
* Resilience to offline and delays
|
|
435
|
+
* Developer-friendly ergonomics
|
|
436
|
+
* Suitable for small devices
|
|
437
|
+
* Approachable but powerful API
|
|
438
|
+
* Long-term maintainability
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
## ๐ License
|
|
443
|
+
|
|
444
|
+
MIT
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## โค๏ธ Acknowledgments
|
|
449
|
+
|
|
450
|
+
jw-automator v3 is a ground-up rethinking of the original jw-automator library, preserving the spirit while strengthening the foundations.
|
|
451
|
+
|
|
452
|
+
If you're building automation logic and want predictable, human-friendly scheduling that survives the real world โ **welcome.**
|