habicron 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,61 @@
1
+ import { Schedule, HabitSummary } from '../core/index.js';
2
+ export { Duration, Jitter, Period } from '../core/index.js';
3
+
4
+ interface ReactControlFlags {
5
+ /** Fire once immediately on start (counts toward `counter`). */
6
+ immediate?: boolean;
7
+ /** Expose `pause`, `resume`, `reset`, `isActive` on the return value. */
8
+ controls?: boolean;
9
+ /** Random source in `[0, 1)`. Defaults to `Math.random`. */
10
+ random?: () => number;
11
+ }
12
+ /** A single inline schedule, or an explicit list of overlapping habits. */
13
+ type UseHabitOptions = ReactControlFlags & (Schedule | {
14
+ habits: Schedule[];
15
+ });
16
+ interface HabitBase {
17
+ /** Total number of times the callback has fired. */
18
+ counter: number;
19
+ /** Earliest upcoming fire across all habits, or `null` when stopped. */
20
+ nextRun: Date | null;
21
+ }
22
+ interface HabitControls {
23
+ isActive: boolean;
24
+ pause: () => void;
25
+ resume: () => void;
26
+ /** Reset `counter` to 0 and, if active, restart all habits from now. */
27
+ reset: () => void;
28
+ }
29
+ /**
30
+ * Schedule a callback on randomized recurring intervals — a "habit" engine.
31
+ *
32
+ * Accurate by default (evenly spaced, anchored to start time, no drift).
33
+ * Add `jitter` to perturb each fire by a bounded random amount.
34
+ *
35
+ * The schedule is captured once when the component mounts; the callback is
36
+ * always read fresh, so closing over changing props is safe. Control members
37
+ * (`pause`/`resume`/`reset`/`isActive`) are returned only with `controls: true`
38
+ * — expressed via overloads, so the return type is exact with no casting.
39
+ *
40
+ * @example
41
+ * const { counter, nextRun, pause } = useHabit(act, {
42
+ * controls: true,
43
+ * every: '20s ~ 4s',
44
+ * })
45
+ */
46
+ declare function useHabit(callback: () => void | Promise<void>, options: UseHabitOptions & {
47
+ controls: true;
48
+ }): HabitBase & HabitControls;
49
+ declare function useHabit(callback: () => void | Promise<void>, options: UseHabitOptions): HabitBase;
50
+ /**
51
+ * Reactively list every registered habit (from `createHabit` / `useHabit`).
52
+ * Re-renders as habits are added, removed, or change state — a ready-made
53
+ * management view.
54
+ *
55
+ * @example
56
+ * const habits = useHabits() // [{ id, name, isActive, counter, nextRun }, …]
57
+ */
58
+ declare function useHabits(): HabitSummary[];
59
+
60
+ export { HabitSummary, Schedule, useHabit, useHabits };
61
+ export type { HabitBase, HabitControls, ReactControlFlags, UseHabitOptions };
@@ -0,0 +1 @@
1
+ import{useRef as f,useState as m,useEffect as v,useCallback as b}from"react";import{createHabit as y,subscribeHabits as H,listHabits as x}from"../core/index.mjs";function S(i,n){const r=f(i);r.current=i;const a=JSON.stringify(n,(e,o)=>typeof o=="function"?void 0:o),u=f(n);u.current=n;const[s,t]=m({counter:0,isActive:!1,nextRun:null}),c=f(null);v(()=>{const e=y(async()=>r.current(),{...u.current,autoStart:!0});c.current=e;const o=()=>{t({counter:e.counter,isActive:e.isActive,nextRun:e.nextRun})},d=e.subscribe(o);return o(),()=>{d(),e.destroy(),c.current=null}},[a]);const p=b(()=>c.current?.pause(),[]),R=b(()=>c.current?.resume(),[]),A=b(()=>c.current?.reset(),[]),l={counter:s.counter,nextRun:s.nextRun};return n.controls?{...l,isActive:s.isActive,pause:p,resume:R,reset:A}:l}function g(){const[i,n]=m([]);return v(()=>{let r=[];const a=()=>{n(x().map(t=>({id:t.id,name:t.name,isActive:t.isActive,counter:t.counter,nextRun:t.nextRun})))},u=()=>{for(const t of r)t();r=x().map(t=>t.subscribe(a)),a()};u();const s=H(u);return()=>{s();for(const t of r)t()}},[]),i}export{S as useHabit,g as useHabits};
@@ -0,0 +1 @@
1
+ "use strict";const vue=require("vue"),core_index=require("../core/index.cjs");function useHabit(r,s){const{controls:o=!1}=s,n=vue.ref(0),u=vue.ref(!1),e=vue.ref(null),t=core_index.createHabit(r,{...s,autoStart:typeof window<"u"}),i=()=>{n.value=t.counter,u.value=t.isActive,e.value=t.nextRun};i();const a=t.subscribe(i);vue.getCurrentScope()&&vue.onScopeDispose(()=>{a(),t.destroy()});const c={counter:vue.readonly(n),nextRun:vue.readonly(e)};return o?{...c,isActive:vue.readonly(u),pause:t.pause,resume:t.resume,reset:t.reset}:c}function useHabits(){const r=vue.ref([]);let s=[];const o=()=>{r.value=core_index.listHabits().map(e=>({id:e.id,name:e.name,isActive:e.isActive,counter:e.counter,nextRun:e.nextRun}))},n=()=>{for(const e of s)e();s=core_index.listHabits().map(e=>e.subscribe(o)),o()};n();const u=core_index.subscribeHabits(n);return vue.getCurrentScope()&&vue.onScopeDispose(()=>{u();for(const e of s)e()}),vue.readonly(r)}exports.useHabit=useHabit,exports.useHabits=useHabits;
@@ -0,0 +1,84 @@
1
+ import { Ref } from 'vue';
2
+ import { Schedule } from '../core/index.cjs';
3
+ export { Duration, HabitSummary, Jitter, Period } from '../core/index.cjs';
4
+
5
+ /**
6
+ * habicron — Vue adapter.
7
+ *
8
+ * Wraps the core engine in Vue refs via `useHabit`.
9
+ */
10
+
11
+ interface VueControlFlags {
12
+ /** Fire once immediately on start (counts toward `counter`). */
13
+ immediate?: boolean;
14
+ /** Expose `pause`, `resume`, `reset`, `isActive` on the return value. */
15
+ controls?: boolean;
16
+ /** Random source in `[0, 1)`. Defaults to `Math.random`. */
17
+ random?: () => number;
18
+ }
19
+ /** A single inline schedule, or an explicit list of overlapping habits. */
20
+ type UseHabitOptions = VueControlFlags & (Schedule | {
21
+ habits: Schedule[];
22
+ });
23
+ interface HabitBase {
24
+ /** Total number of times the callback has fired. */
25
+ readonly counter: Readonly<Ref<number>>;
26
+ /** Earliest upcoming fire across all habits, or `null` when stopped. */
27
+ readonly nextRun: Readonly<Ref<Date | null>>;
28
+ }
29
+ interface HabitControls {
30
+ readonly isActive: Readonly<Ref<boolean>>;
31
+ pause: () => void;
32
+ resume: () => void;
33
+ /** Reset `counter` to 0 and, if active, restart all habits from now. */
34
+ reset: () => void;
35
+ }
36
+ /**
37
+ * Schedule a callback on randomized recurring intervals — a "habit" engine.
38
+ *
39
+ * Accurate by default (evenly spaced, anchored to start time, no drift).
40
+ * Add `jitter` to perturb each fire by a bounded random amount. Control members
41
+ * (`pause`/`resume`/`reset`/`isActive`) are returned only with `controls: true`
42
+ * — expressed via overloads, so the return type is exact with no casting.
43
+ *
44
+ * @example
45
+ * useHabit(act, { every: '2h ~ 5m' })
46
+ *
47
+ * @example
48
+ * const { counter, nextRun, pause } = useHabit(act, {
49
+ * controls: true,
50
+ * habits: [
51
+ * { every: '20s', jitter: ['3s', '5s'] },
52
+ * { times: 2, per: 'day', jitter: '2h' },
53
+ * ],
54
+ * })
55
+ */
56
+ declare function useHabit(callback: () => void | Promise<void>, options: UseHabitOptions & {
57
+ controls: true;
58
+ }): HabitBase & HabitControls;
59
+ declare function useHabit(callback: () => void | Promise<void>, options: UseHabitOptions): HabitBase;
60
+ /**
61
+ * Reactively list every registered habit (from `createHabit` / `useHabit`).
62
+ * The returned ref updates as habits are added, removed, or change state —
63
+ * a ready-made management view.
64
+ *
65
+ * @example
66
+ * const habits = useHabits()
67
+ * // habits.value -> [{ id, name, isActive, counter, nextRun }, …]
68
+ */
69
+ declare function useHabits(): Readonly<Ref<readonly {
70
+ readonly id: string;
71
+ readonly name: string | undefined;
72
+ readonly isActive: boolean;
73
+ readonly counter: number;
74
+ readonly nextRun: Date | null;
75
+ }[], readonly {
76
+ readonly id: string;
77
+ readonly name: string | undefined;
78
+ readonly isActive: boolean;
79
+ readonly counter: number;
80
+ readonly nextRun: Date | null;
81
+ }[]>>;
82
+
83
+ export { Schedule, useHabit, useHabits };
84
+ export type { HabitBase, HabitControls, UseHabitOptions, VueControlFlags };
@@ -0,0 +1,84 @@
1
+ import { Ref } from 'vue';
2
+ import { Schedule } from '../core/index.mjs';
3
+ export { Duration, HabitSummary, Jitter, Period } from '../core/index.mjs';
4
+
5
+ /**
6
+ * habicron — Vue adapter.
7
+ *
8
+ * Wraps the core engine in Vue refs via `useHabit`.
9
+ */
10
+
11
+ interface VueControlFlags {
12
+ /** Fire once immediately on start (counts toward `counter`). */
13
+ immediate?: boolean;
14
+ /** Expose `pause`, `resume`, `reset`, `isActive` on the return value. */
15
+ controls?: boolean;
16
+ /** Random source in `[0, 1)`. Defaults to `Math.random`. */
17
+ random?: () => number;
18
+ }
19
+ /** A single inline schedule, or an explicit list of overlapping habits. */
20
+ type UseHabitOptions = VueControlFlags & (Schedule | {
21
+ habits: Schedule[];
22
+ });
23
+ interface HabitBase {
24
+ /** Total number of times the callback has fired. */
25
+ readonly counter: Readonly<Ref<number>>;
26
+ /** Earliest upcoming fire across all habits, or `null` when stopped. */
27
+ readonly nextRun: Readonly<Ref<Date | null>>;
28
+ }
29
+ interface HabitControls {
30
+ readonly isActive: Readonly<Ref<boolean>>;
31
+ pause: () => void;
32
+ resume: () => void;
33
+ /** Reset `counter` to 0 and, if active, restart all habits from now. */
34
+ reset: () => void;
35
+ }
36
+ /**
37
+ * Schedule a callback on randomized recurring intervals — a "habit" engine.
38
+ *
39
+ * Accurate by default (evenly spaced, anchored to start time, no drift).
40
+ * Add `jitter` to perturb each fire by a bounded random amount. Control members
41
+ * (`pause`/`resume`/`reset`/`isActive`) are returned only with `controls: true`
42
+ * — expressed via overloads, so the return type is exact with no casting.
43
+ *
44
+ * @example
45
+ * useHabit(act, { every: '2h ~ 5m' })
46
+ *
47
+ * @example
48
+ * const { counter, nextRun, pause } = useHabit(act, {
49
+ * controls: true,
50
+ * habits: [
51
+ * { every: '20s', jitter: ['3s', '5s'] },
52
+ * { times: 2, per: 'day', jitter: '2h' },
53
+ * ],
54
+ * })
55
+ */
56
+ declare function useHabit(callback: () => void | Promise<void>, options: UseHabitOptions & {
57
+ controls: true;
58
+ }): HabitBase & HabitControls;
59
+ declare function useHabit(callback: () => void | Promise<void>, options: UseHabitOptions): HabitBase;
60
+ /**
61
+ * Reactively list every registered habit (from `createHabit` / `useHabit`).
62
+ * The returned ref updates as habits are added, removed, or change state —
63
+ * a ready-made management view.
64
+ *
65
+ * @example
66
+ * const habits = useHabits()
67
+ * // habits.value -> [{ id, name, isActive, counter, nextRun }, …]
68
+ */
69
+ declare function useHabits(): Readonly<Ref<readonly {
70
+ readonly id: string;
71
+ readonly name: string | undefined;
72
+ readonly isActive: boolean;
73
+ readonly counter: number;
74
+ readonly nextRun: Date | null;
75
+ }[], readonly {
76
+ readonly id: string;
77
+ readonly name: string | undefined;
78
+ readonly isActive: boolean;
79
+ readonly counter: number;
80
+ readonly nextRun: Date | null;
81
+ }[]>>;
82
+
83
+ export { Schedule, useHabit, useHabits };
84
+ export type { HabitBase, HabitControls, UseHabitOptions, VueControlFlags };
@@ -0,0 +1,84 @@
1
+ import { Ref } from 'vue';
2
+ import { Schedule } from '../core/index.js';
3
+ export { Duration, HabitSummary, Jitter, Period } from '../core/index.js';
4
+
5
+ /**
6
+ * habicron — Vue adapter.
7
+ *
8
+ * Wraps the core engine in Vue refs via `useHabit`.
9
+ */
10
+
11
+ interface VueControlFlags {
12
+ /** Fire once immediately on start (counts toward `counter`). */
13
+ immediate?: boolean;
14
+ /** Expose `pause`, `resume`, `reset`, `isActive` on the return value. */
15
+ controls?: boolean;
16
+ /** Random source in `[0, 1)`. Defaults to `Math.random`. */
17
+ random?: () => number;
18
+ }
19
+ /** A single inline schedule, or an explicit list of overlapping habits. */
20
+ type UseHabitOptions = VueControlFlags & (Schedule | {
21
+ habits: Schedule[];
22
+ });
23
+ interface HabitBase {
24
+ /** Total number of times the callback has fired. */
25
+ readonly counter: Readonly<Ref<number>>;
26
+ /** Earliest upcoming fire across all habits, or `null` when stopped. */
27
+ readonly nextRun: Readonly<Ref<Date | null>>;
28
+ }
29
+ interface HabitControls {
30
+ readonly isActive: Readonly<Ref<boolean>>;
31
+ pause: () => void;
32
+ resume: () => void;
33
+ /** Reset `counter` to 0 and, if active, restart all habits from now. */
34
+ reset: () => void;
35
+ }
36
+ /**
37
+ * Schedule a callback on randomized recurring intervals — a "habit" engine.
38
+ *
39
+ * Accurate by default (evenly spaced, anchored to start time, no drift).
40
+ * Add `jitter` to perturb each fire by a bounded random amount. Control members
41
+ * (`pause`/`resume`/`reset`/`isActive`) are returned only with `controls: true`
42
+ * — expressed via overloads, so the return type is exact with no casting.
43
+ *
44
+ * @example
45
+ * useHabit(act, { every: '2h ~ 5m' })
46
+ *
47
+ * @example
48
+ * const { counter, nextRun, pause } = useHabit(act, {
49
+ * controls: true,
50
+ * habits: [
51
+ * { every: '20s', jitter: ['3s', '5s'] },
52
+ * { times: 2, per: 'day', jitter: '2h' },
53
+ * ],
54
+ * })
55
+ */
56
+ declare function useHabit(callback: () => void | Promise<void>, options: UseHabitOptions & {
57
+ controls: true;
58
+ }): HabitBase & HabitControls;
59
+ declare function useHabit(callback: () => void | Promise<void>, options: UseHabitOptions): HabitBase;
60
+ /**
61
+ * Reactively list every registered habit (from `createHabit` / `useHabit`).
62
+ * The returned ref updates as habits are added, removed, or change state —
63
+ * a ready-made management view.
64
+ *
65
+ * @example
66
+ * const habits = useHabits()
67
+ * // habits.value -> [{ id, name, isActive, counter, nextRun }, …]
68
+ */
69
+ declare function useHabits(): Readonly<Ref<readonly {
70
+ readonly id: string;
71
+ readonly name: string | undefined;
72
+ readonly isActive: boolean;
73
+ readonly counter: number;
74
+ readonly nextRun: Date | null;
75
+ }[], readonly {
76
+ readonly id: string;
77
+ readonly name: string | undefined;
78
+ readonly isActive: boolean;
79
+ readonly counter: number;
80
+ readonly nextRun: Date | null;
81
+ }[]>>;
82
+
83
+ export { Schedule, useHabit, useHabits };
84
+ export type { HabitBase, HabitControls, UseHabitOptions, VueControlFlags };
@@ -0,0 +1 @@
1
+ import{ref as i,getCurrentScope as m,onScopeDispose as p,readonly as c}from"vue";import{createHabit as v,subscribeHabits as d,listHabits as f}from"../core/index.mjs";function x(o,n){const{controls:r=!1}=n,s=i(0),u=i(!1),e=i(null),t=v(o,{...n,autoStart:typeof window<"u"}),a=()=>{s.value=t.counter,u.value=t.isActive,e.value=t.nextRun};a();const l=t.subscribe(a);m()&&p(()=>{l(),t.destroy()});const b={counter:c(s),nextRun:c(e)};return r?{...b,isActive:c(u),pause:t.pause,resume:t.resume,reset:t.reset}:b}function H(){const o=i([]);let n=[];const r=()=>{o.value=f().map(e=>({id:e.id,name:e.name,isActive:e.isActive,counter:e.counter,nextRun:e.nextRun}))},s=()=>{for(const e of n)e();n=f().map(e=>e.subscribe(r)),r()};s();const u=d(s);return m()&&p(()=>{u();for(const e of n)e()}),c(o)}export{x as useHabit,H as useHabits};
package/package.json ADDED
@@ -0,0 +1,125 @@
1
+ {
2
+ "name": "habicron",
3
+ "type": "module",
4
+ "version": "0.2.0",
5
+ "description": "Schedule callbacks on randomized recurring intervals — habits, not cronjobs. Accurate by default, jittered on demand, with no drift. Works in Node, Vue, React, and the CLI.",
6
+ "author": "thecodeorigin <nguyenhuunguyeny.ny@gmail.com>",
7
+ "license": "MIT",
8
+ "homepage": "https://github.com/imrim12/habicron#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/imrim12/habicron.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/imrim12/habicron/issues"
15
+ },
16
+ "keywords": [
17
+ "cron",
18
+ "cronjob",
19
+ "scheduler",
20
+ "schedule",
21
+ "interval",
22
+ "setInterval",
23
+ "jitter",
24
+ "random",
25
+ "habit",
26
+ "recurring",
27
+ "timer",
28
+ "vue",
29
+ "react",
30
+ "composable",
31
+ "hook",
32
+ "cli"
33
+ ],
34
+ "sideEffects": false,
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/node/index.d.ts",
38
+ "import": "./dist/node/index.mjs",
39
+ "require": "./dist/node/index.cjs"
40
+ },
41
+ "./core": {
42
+ "types": "./dist/core/index.d.ts",
43
+ "import": "./dist/core/index.mjs",
44
+ "require": "./dist/core/index.cjs"
45
+ },
46
+ "./browser": {
47
+ "types": "./dist/browser/index.d.ts",
48
+ "import": "./dist/browser/index.mjs",
49
+ "require": "./dist/browser/index.cjs"
50
+ },
51
+ "./node": {
52
+ "types": "./dist/node/index.d.ts",
53
+ "import": "./dist/node/index.mjs",
54
+ "require": "./dist/node/index.cjs"
55
+ },
56
+ "./vue": {
57
+ "types": "./dist/vue/index.d.ts",
58
+ "import": "./dist/vue/index.mjs",
59
+ "require": "./dist/vue/index.cjs"
60
+ },
61
+ "./react": {
62
+ "types": "./dist/react/index.d.ts",
63
+ "import": "./dist/react/index.mjs",
64
+ "require": "./dist/react/index.cjs"
65
+ },
66
+ "./package.json": "./package.json"
67
+ },
68
+ "main": "./dist/node/index.cjs",
69
+ "module": "./dist/node/index.mjs",
70
+ "types": "./dist/node/index.d.ts",
71
+ "bin": {
72
+ "habit": "./dist/cli/index.mjs"
73
+ },
74
+ "files": [
75
+ "LICENSE",
76
+ "README.md",
77
+ "dist"
78
+ ],
79
+ "engines": {
80
+ "node": ">=18"
81
+ },
82
+ "publishConfig": {
83
+ "access": "public"
84
+ },
85
+ "peerDependencies": {
86
+ "react": ">=17",
87
+ "vue": ">=3"
88
+ },
89
+ "peerDependenciesMeta": {
90
+ "react": {
91
+ "optional": true
92
+ },
93
+ "vue": {
94
+ "optional": true
95
+ }
96
+ },
97
+ "devDependencies": {
98
+ "@antfu/eslint-config": "^9.0.0",
99
+ "@eslint-react/eslint-plugin": "^5.9.0",
100
+ "@testing-library/react": "^16.1.0",
101
+ "@types/node": "^22.10.2",
102
+ "@types/react": "^19.0.2",
103
+ "@vitest/coverage-v8": "^2.1.8",
104
+ "eslint": "^10.5.0",
105
+ "eslint-plugin-react-hooks": "^7.1.1",
106
+ "eslint-plugin-react-refresh": "^0.5.3",
107
+ "eslint-plugin-vue": "^10.9.2",
108
+ "jsdom": "^25.0.1",
109
+ "react": "^19.0.0",
110
+ "react-dom": "^19.0.0",
111
+ "typescript": "^5.7.2",
112
+ "unbuild": "^2.0.0",
113
+ "vitest": "^2.1.8",
114
+ "vue": "^3.5.13",
115
+ "vue-eslint-parser": "^10.4.1"
116
+ },
117
+ "scripts": {
118
+ "lint": "eslint . --max-warnings 0",
119
+ "lint:fix": "eslint . --fix",
120
+ "typecheck": "tsc --noEmit",
121
+ "test": "vitest run",
122
+ "test:watch": "vitest",
123
+ "test:coverage": "vitest run --coverage"
124
+ }
125
+ }