paneful 0.9.17 → 0.9.19

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,189 @@
1
+ /**
2
+ * Minimal cron matcher. Supports 5-field standard cron:
3
+ * minute hour day-of-month month day-of-week
4
+ *
5
+ * Field syntax:
6
+ * * (any)
7
+ * N (literal)
8
+ * N,M,O (list)
9
+ * N-M (range)
10
+ * *\/N (step)
11
+ * N-M/K (stepped range)
12
+ *
13
+ * Day-of-week: 0-6 (0=Sunday).
14
+ */
15
+ const RANGES = [
16
+ [0, 59], // minute
17
+ [0, 23], // hour
18
+ [1, 31], // day of month
19
+ [1, 12], // month
20
+ [0, 6], // day of week
21
+ ];
22
+ function parseField(field, min, max) {
23
+ if (field === '*')
24
+ return 'any';
25
+ const values = new Set();
26
+ for (const part of field.split(',')) {
27
+ let stepStr;
28
+ let rangeStr = part;
29
+ const slashIdx = part.indexOf('/');
30
+ if (slashIdx >= 0) {
31
+ rangeStr = part.slice(0, slashIdx);
32
+ stepStr = part.slice(slashIdx + 1);
33
+ }
34
+ const step = stepStr ? parseInt(stepStr, 10) : 1;
35
+ if (!Number.isFinite(step) || step <= 0)
36
+ continue;
37
+ let lo;
38
+ let hi;
39
+ if (rangeStr === '*') {
40
+ lo = min;
41
+ hi = max;
42
+ }
43
+ else if (rangeStr.includes('-')) {
44
+ const [a, b] = rangeStr.split('-');
45
+ lo = parseInt(a, 10);
46
+ hi = parseInt(b, 10);
47
+ }
48
+ else {
49
+ lo = parseInt(rangeStr, 10);
50
+ hi = lo;
51
+ }
52
+ if (!Number.isFinite(lo) || !Number.isFinite(hi))
53
+ continue;
54
+ for (let v = lo; v <= hi; v += step) {
55
+ if (v >= min && v <= max)
56
+ values.add(v);
57
+ }
58
+ }
59
+ return values;
60
+ }
61
+ export function parseCron(expr) {
62
+ // Aliases
63
+ let trimmed = expr.trim();
64
+ switch (trimmed) {
65
+ case '@yearly':
66
+ case '@annually':
67
+ trimmed = '0 0 1 1 *';
68
+ break;
69
+ case '@monthly':
70
+ trimmed = '0 0 1 * *';
71
+ break;
72
+ case '@weekly':
73
+ trimmed = '0 0 * * 0';
74
+ break;
75
+ case '@daily':
76
+ case '@midnight':
77
+ trimmed = '0 0 * * *';
78
+ break;
79
+ case '@hourly':
80
+ trimmed = '0 * * * *';
81
+ break;
82
+ }
83
+ const fields = trimmed.split(/\s+/);
84
+ if (fields.length !== 5)
85
+ return null;
86
+ const parsed = {
87
+ minute: parseField(fields[0], RANGES[0][0], RANGES[0][1]),
88
+ hour: parseField(fields[1], RANGES[1][0], RANGES[1][1]),
89
+ dom: parseField(fields[2], RANGES[2][0], RANGES[2][1]),
90
+ month: parseField(fields[3], RANGES[3][0], RANGES[3][1]),
91
+ dow: parseField(fields[4], RANGES[4][0], RANGES[4][1]),
92
+ };
93
+ return parsed;
94
+ }
95
+ function matches(set, val) {
96
+ return set === 'any' || set.has(val);
97
+ }
98
+ export function cronMatches(expr, date) {
99
+ const parsed = parseCron(expr);
100
+ if (!parsed)
101
+ return false;
102
+ return (matches(parsed.minute, date.getMinutes()) &&
103
+ matches(parsed.hour, date.getHours()) &&
104
+ matches(parsed.dom, date.getDate()) &&
105
+ matches(parsed.month, date.getMonth() + 1) &&
106
+ matches(parsed.dow, date.getDay()));
107
+ }
108
+ /**
109
+ * Compute the next firing time at-or-after `from`, scanning up to
110
+ * `maxMinutes` ahead. Used for "next run in X" UI hints.
111
+ */
112
+ export function nextRun(expr, from, maxMinutes = 60 * 24 * 7) {
113
+ const parsed = parseCron(expr);
114
+ if (!parsed)
115
+ return null;
116
+ const cursor = new Date(from);
117
+ cursor.setSeconds(0, 0);
118
+ cursor.setMinutes(cursor.getMinutes() + 1);
119
+ for (let i = 0; i < maxMinutes; i++) {
120
+ if (matches(parsed.minute, cursor.getMinutes()) &&
121
+ matches(parsed.hour, cursor.getHours()) &&
122
+ matches(parsed.dom, cursor.getDate()) &&
123
+ matches(parsed.month, cursor.getMonth() + 1) &&
124
+ matches(parsed.dow, cursor.getDay())) {
125
+ return cursor;
126
+ }
127
+ cursor.setMinutes(cursor.getMinutes() + 1);
128
+ }
129
+ return null;
130
+ }
131
+ export class Scheduler {
132
+ store;
133
+ onFire;
134
+ timer = null;
135
+ lastTickMinute = null;
136
+ destroyed = false;
137
+ constructor(store, onFire) {
138
+ this.store = store;
139
+ this.onFire = onFire;
140
+ }
141
+ start() {
142
+ if (this.destroyed || this.timer)
143
+ return;
144
+ this.scheduleNextTick();
145
+ }
146
+ stop() {
147
+ if (this.timer) {
148
+ clearTimeout(this.timer);
149
+ this.timer = null;
150
+ }
151
+ }
152
+ destroy() {
153
+ this.destroyed = true;
154
+ this.stop();
155
+ }
156
+ scheduleNextTick() {
157
+ if (this.destroyed)
158
+ return;
159
+ const now = new Date();
160
+ // Sleep until the start of the next minute (+50ms slack to avoid edge cases)
161
+ const msToNextMinute = (60 - now.getSeconds()) * 1000 - now.getMilliseconds() + 50;
162
+ this.timer = setTimeout(() => {
163
+ this.tick();
164
+ this.scheduleNextTick();
165
+ }, msToNextMinute);
166
+ }
167
+ tick() {
168
+ if (this.destroyed)
169
+ return;
170
+ const now = new Date();
171
+ now.setSeconds(0, 0);
172
+ const minuteKey = now.getTime();
173
+ if (minuteKey === this.lastTickMinute)
174
+ return;
175
+ this.lastTickMinute = minuteKey;
176
+ for (const job of this.store.listJobs()) {
177
+ if (!job.enabled)
178
+ continue;
179
+ try {
180
+ if (cronMatches(job.cron, now)) {
181
+ this.onFire(job);
182
+ }
183
+ }
184
+ catch (e) {
185
+ console.error('schedule check failed for', job.id, e);
186
+ }
187
+ }
188
+ }
189
+ }
@@ -6,6 +6,8 @@ const DEFAULTS = {
6
6
  theme: 'system',
7
7
  sidebarWidth: 224,
8
8
  editorSyncEnabled: true,
9
+ sourceControlOpen: false,
10
+ sourceControlWidth: 360,
9
11
  },
10
12
  activeProjectId: null,
11
13
  };
@@ -24,6 +26,8 @@ export class SettingsStore {
24
26
  theme: raw.ui?.theme ?? DEFAULTS.ui.theme,
25
27
  sidebarWidth: raw.ui?.sidebarWidth ?? DEFAULTS.ui.sidebarWidth,
26
28
  editorSyncEnabled: raw.ui?.editorSyncEnabled ?? DEFAULTS.ui.editorSyncEnabled,
29
+ sourceControlOpen: raw.ui?.sourceControlOpen ?? DEFAULTS.ui.sourceControlOpen,
30
+ sourceControlWidth: raw.ui?.sourceControlWidth ?? DEFAULTS.ui.sourceControlWidth,
27
31
  },
28
32
  activeProjectId: raw.activeProjectId ?? DEFAULTS.activeProjectId,
29
33
  };