@skygraph/core 0.0.0-placeholder.0 → 0.5.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/LICENSE +21 -0
- package/README.md +42 -6
- package/dist/calendar.cjs +404 -0
- package/dist/calendar.cjs.map +1 -0
- package/dist/calendar.d.cts +209 -0
- package/dist/calendar.d.ts +209 -0
- package/dist/calendar.js +12 -0
- package/dist/calendar.js.map +1 -0
- package/dist/chunk-5CCD5Q4B.js +36 -0
- package/dist/chunk-5CCD5Q4B.js.map +1 -0
- package/dist/chunk-7FA2TBSZ.js +1211 -0
- package/dist/chunk-7FA2TBSZ.js.map +1 -0
- package/dist/chunk-AB3RLBLW.js +357 -0
- package/dist/chunk-AB3RLBLW.js.map +1 -0
- package/dist/chunk-GEMALROQ.js +258 -0
- package/dist/chunk-GEMALROQ.js.map +1 -0
- package/dist/chunk-IY6LJU3L.js +256 -0
- package/dist/chunk-IY6LJU3L.js.map +1 -0
- package/dist/chunk-KMGRNPLG.js +453 -0
- package/dist/chunk-KMGRNPLG.js.map +1 -0
- package/dist/chunk-Y7FTBBYX.js +397 -0
- package/dist/chunk-Y7FTBBYX.js.map +1 -0
- package/dist/chunk-ZWB7JGAJ.js +488 -0
- package/dist/chunk-ZWB7JGAJ.js.map +1 -0
- package/dist/form.cjs +498 -0
- package/dist/form.cjs.map +1 -0
- package/dist/form.d.cts +130 -0
- package/dist/form.d.ts +130 -0
- package/dist/form.js +8 -0
- package/dist/form.js.map +1 -0
- package/dist/graph.cjs +1269 -0
- package/dist/graph.cjs.map +1 -0
- package/dist/graph.d.cts +671 -0
- package/dist/graph.d.ts +671 -0
- package/dist/graph.js +34 -0
- package/dist/graph.js.map +1 -0
- package/dist/index.cjs +3860 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +144 -0
- package/dist/index.d.ts +144 -0
- package/dist/index.js +477 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime-internal.cjs +339 -0
- package/dist/runtime-internal.cjs.map +1 -0
- package/dist/runtime-internal.d.cts +130 -0
- package/dist/runtime-internal.d.ts +130 -0
- package/dist/runtime-internal.js +67 -0
- package/dist/runtime-internal.js.map +1 -0
- package/dist/table.cjs +537 -0
- package/dist/table.cjs.map +1 -0
- package/dist/table.d.cts +215 -0
- package/dist/table.d.ts +215 -0
- package/dist/table.js +16 -0
- package/dist/table.js.map +1 -0
- package/dist/tree.cjs +442 -0
- package/dist/tree.cjs.map +1 -0
- package/dist/tree.d.cts +76 -0
- package/dist/tree.d.ts +76 -0
- package/dist/tree.js +8 -0
- package/dist/tree.js.map +1 -0
- package/dist/types-B-rKQJ4R.d.cts +35 -0
- package/dist/types-B-rKQJ4R.d.ts +35 -0
- package/dist/virtual.cjs +283 -0
- package/dist/virtual.cjs.map +1 -0
- package/dist/virtual.d.cts +96 -0
- package/dist/virtual.d.ts +96 -0
- package/dist/virtual.js +9 -0
- package/dist/virtual.js.map +1 -0
- package/package.json +98 -18
- package/index.js +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SkyGraph 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
CHANGED
|
@@ -1,12 +1,48 @@
|
|
|
1
1
|
# @skygraph/core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Pure-TypeScript runtime for [SkyGraph](https://skygraph.ruslansinkevich.ru/) — the engine that drives forms, tables, trees, virtual lists, graphs and calendars in the React and Vue adapters. Zero runtime dependencies.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> **Most users don't install this package directly.** Use one of the meta-packages:
|
|
6
|
+
>
|
|
7
|
+
> ```bash
|
|
8
|
+
> npm install skygraph-react # React + core + styles in one
|
|
9
|
+
> npm install skygraph-vue # Vue 3 + core + styles in one
|
|
10
|
+
> ```
|
|
11
|
+
>
|
|
12
|
+
> Install `@skygraph/core` directly only when you want to drive the engine without an adapter (custom renderer, headless integration, server-side validation).
|
|
6
13
|
|
|
7
|
-
|
|
14
|
+
## Install
|
|
8
15
|
|
|
9
|
-
|
|
10
|
-
|
|
16
|
+
```bash
|
|
17
|
+
npm install @skygraph/core
|
|
18
|
+
```
|
|
11
19
|
|
|
12
|
-
|
|
20
|
+
## What's inside
|
|
21
|
+
|
|
22
|
+
- **Runtime:** reactive `Store`, `Batch`, `Transaction`, `Computed`, `Scheduler`, `Middleware`.
|
|
23
|
+
- **Engines:** `FormEngine`, `TableEngine`, `TreeEngine`, `VirtualScroll`, `GraphEngine`, `CalendarEngine`.
|
|
24
|
+
- **Subpath exports:** `@skygraph/core/{form,table,tree,virtual,graph,calendar}` for cherry-picking.
|
|
25
|
+
|
|
26
|
+
## Architecture
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
@skygraph/core (pure TS, 0 deps)
|
|
30
|
+
├── runtime/ — Store, Batch, Transaction, Computed, Scheduler, Middleware
|
|
31
|
+
└── engines/
|
|
32
|
+
├── FormEngine — validation, deps, lists
|
|
33
|
+
├── TableEngine — sort, filter, group, virtual
|
|
34
|
+
├── TreeEngine — check, expand, drag
|
|
35
|
+
├── VirtualScroll — windowed rendering
|
|
36
|
+
├── GraphEngine — nodes, edges, routing
|
|
37
|
+
└── CalendarEngine — assignments, conflicts
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
See [`ARCHITECTURE.md`](https://github.com/RuslanSinkevich/skygraph-public/blob/master/ARCHITECTURE.md) in the repo for the full breakdown.
|
|
41
|
+
|
|
42
|
+
## Repository
|
|
43
|
+
|
|
44
|
+
[github.com/RuslanSinkevich/skygraph-public](https://github.com/RuslanSinkevich/skygraph-public)
|
|
45
|
+
|
|
46
|
+
## License
|
|
47
|
+
|
|
48
|
+
MIT
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/engines/calendar/index.ts
|
|
21
|
+
var calendar_exports = {};
|
|
22
|
+
__export(calendar_exports, {
|
|
23
|
+
createCalendar: () => createCalendar,
|
|
24
|
+
detectConflicts: () => detectConflicts,
|
|
25
|
+
isAvailable: () => isAvailable
|
|
26
|
+
});
|
|
27
|
+
module.exports = __toCommonJS(calendar_exports);
|
|
28
|
+
|
|
29
|
+
// src/engines/namespaces.ts
|
|
30
|
+
var reserved = /* @__PURE__ */ new Map();
|
|
31
|
+
function reservePrefix(engine, prefix) {
|
|
32
|
+
if (!prefix.startsWith("$") || !prefix.endsWith(".")) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`[skygraph] engine "${engine}" tried to reserve invalid prefix "${prefix}". Engine prefixes must start with "$" and end with ".".`
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
const existing = reserved.get(prefix);
|
|
38
|
+
if (existing !== void 0 && existing !== engine) {
|
|
39
|
+
throw new Error(
|
|
40
|
+
`[skygraph] prefix "${prefix}" is already owned by engine "${existing}", cannot reserve it for "${engine}". Pick a different prefix or update the registry.`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
reserved.set(prefix, engine);
|
|
44
|
+
return prefix;
|
|
45
|
+
}
|
|
46
|
+
var FORM_META_PREFIX = reservePrefix("form", "$meta.");
|
|
47
|
+
var TABLE_PREFIX = reservePrefix("table", "$table.");
|
|
48
|
+
var TREE_PREFIX = reservePrefix("tree", "$tree.");
|
|
49
|
+
var GRAPH_PREFIX = reservePrefix("graph", "$graph.");
|
|
50
|
+
var CALENDAR_PREFIX = reservePrefix("calendar", "$calendar.");
|
|
51
|
+
|
|
52
|
+
// src/engines/calendar/availability.ts
|
|
53
|
+
var DAY_MS = 864e5;
|
|
54
|
+
function parseHHMM(s, fallback) {
|
|
55
|
+
if (s === void 0) return fallback;
|
|
56
|
+
const m = /^(\d{1,2}):(\d{2})$/.exec(s);
|
|
57
|
+
if (!m) return fallback;
|
|
58
|
+
const h = Number(m[1]);
|
|
59
|
+
const mm = Number(m[2]);
|
|
60
|
+
if (!Number.isFinite(h) || !Number.isFinite(mm)) return fallback;
|
|
61
|
+
if (h < 0 || h > 24 || mm < 0 || mm >= 60) return fallback;
|
|
62
|
+
return h * 36e5 + mm * 6e4;
|
|
63
|
+
}
|
|
64
|
+
function rulesFor(resourceId, rules) {
|
|
65
|
+
return rules.filter((r) => r.resourceId === resourceId);
|
|
66
|
+
}
|
|
67
|
+
function dayBucket(t) {
|
|
68
|
+
const d = new Date(t);
|
|
69
|
+
const dayStart = Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate());
|
|
70
|
+
const dayOfWeek = new Date(dayStart).getUTCDay();
|
|
71
|
+
return { dayStart, dayOfWeek };
|
|
72
|
+
}
|
|
73
|
+
function isAvailable(resource, range, rules) {
|
|
74
|
+
if (range.end <= range.start) return true;
|
|
75
|
+
const own = rulesFor(resource.id, rules);
|
|
76
|
+
if (own.length === 0) return true;
|
|
77
|
+
let cursor = range.start;
|
|
78
|
+
while (cursor < range.end) {
|
|
79
|
+
const { dayStart, dayOfWeek } = dayBucket(cursor);
|
|
80
|
+
const dayEnd = dayStart + DAY_MS;
|
|
81
|
+
const sliceEnd = Math.min(range.end, dayEnd);
|
|
82
|
+
const dayRules = own.filter(
|
|
83
|
+
(r) => r.dayOfWeek === void 0 || r.dayOfWeek === dayOfWeek
|
|
84
|
+
);
|
|
85
|
+
if (dayRules.length === 0) return false;
|
|
86
|
+
const intervals = dayRules.map((r) => {
|
|
87
|
+
const fromMs = parseHHMM(r.from, 0);
|
|
88
|
+
const toMs = parseHHMM(r.to, DAY_MS);
|
|
89
|
+
return { from: dayStart + fromMs, to: dayStart + toMs };
|
|
90
|
+
}).filter((w) => w.to > w.from).sort((a, b) => a.from - b.from);
|
|
91
|
+
if (intervals.length === 0) return false;
|
|
92
|
+
const merged = [];
|
|
93
|
+
for (const w of intervals) {
|
|
94
|
+
const last = merged[merged.length - 1];
|
|
95
|
+
if (last && w.from <= last.to) {
|
|
96
|
+
last.to = Math.max(last.to, w.to);
|
|
97
|
+
} else {
|
|
98
|
+
merged.push({ ...w });
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
let needed = cursor;
|
|
102
|
+
for (const w of merged) {
|
|
103
|
+
if (w.from > needed) return false;
|
|
104
|
+
if (w.to >= sliceEnd) {
|
|
105
|
+
needed = sliceEnd;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
needed = Math.max(needed, w.to);
|
|
109
|
+
}
|
|
110
|
+
if (needed < sliceEnd) return false;
|
|
111
|
+
cursor = sliceEnd;
|
|
112
|
+
}
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// src/engines/calendar/conflicts.ts
|
|
117
|
+
var DAY_MS2 = 864e5;
|
|
118
|
+
var STEP_MS = {
|
|
119
|
+
day: DAY_MS2,
|
|
120
|
+
week: 7 * DAY_MS2,
|
|
121
|
+
month: 30 * DAY_MS2
|
|
122
|
+
};
|
|
123
|
+
function makeId(reason, ids) {
|
|
124
|
+
return `${reason}:${[...ids].sort().join(",")}`;
|
|
125
|
+
}
|
|
126
|
+
function pushConflict(out, reason, ids) {
|
|
127
|
+
if (ids.length === 0) return;
|
|
128
|
+
const sorted = [...ids].sort();
|
|
129
|
+
const id = makeId(reason, sorted);
|
|
130
|
+
if (out.has(id)) return;
|
|
131
|
+
out.set(id, { id, reason, assignmentIds: sorted });
|
|
132
|
+
}
|
|
133
|
+
function detectConflicts(state, opts = {}) {
|
|
134
|
+
const scale = opts.scale ?? "day";
|
|
135
|
+
const step = STEP_MS[scale];
|
|
136
|
+
const rules = opts.rules ?? [];
|
|
137
|
+
const conflicts = /* @__PURE__ */ new Map();
|
|
138
|
+
const byResource = /* @__PURE__ */ new Map();
|
|
139
|
+
for (const a of state.assignments) {
|
|
140
|
+
let bucket = byResource.get(a.resourceId);
|
|
141
|
+
if (!bucket) {
|
|
142
|
+
bucket = [];
|
|
143
|
+
byResource.set(a.resourceId, bucket);
|
|
144
|
+
}
|
|
145
|
+
bucket.push(a);
|
|
146
|
+
}
|
|
147
|
+
for (const list of byResource.values()) {
|
|
148
|
+
const sorted = [...list].sort((x, y) => x.start - y.start);
|
|
149
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
150
|
+
const a = sorted[i];
|
|
151
|
+
for (let j = i + 1; j < sorted.length; j++) {
|
|
152
|
+
const b = sorted[j];
|
|
153
|
+
if (b.start >= a.end) break;
|
|
154
|
+
pushConflict(conflicts, "overlap", [a.id, b.id]);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const resourceById = /* @__PURE__ */ new Map();
|
|
159
|
+
for (const r of state.resources) resourceById.set(r.id, r);
|
|
160
|
+
for (const [rid, list] of byResource) {
|
|
161
|
+
const resource = resourceById.get(rid);
|
|
162
|
+
if (!resource || resource.capacityPerSlot === void 0) continue;
|
|
163
|
+
const cap = Math.max(0, Math.floor(resource.capacityPerSlot));
|
|
164
|
+
const slotMap = /* @__PURE__ */ new Map();
|
|
165
|
+
for (const a of list) {
|
|
166
|
+
const lo = Math.floor(a.start / step) * step;
|
|
167
|
+
const hi = Math.ceil(a.end / step) * step;
|
|
168
|
+
for (let s = lo; s < hi; s += step) {
|
|
169
|
+
let bucket = slotMap.get(s);
|
|
170
|
+
if (!bucket) {
|
|
171
|
+
bucket = [];
|
|
172
|
+
slotMap.set(s, bucket);
|
|
173
|
+
}
|
|
174
|
+
bucket.push(a.id);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
for (const ids of slotMap.values()) {
|
|
178
|
+
if (ids.length > cap) {
|
|
179
|
+
pushConflict(conflicts, "over-capacity", ids);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (rules.length > 0) {
|
|
184
|
+
for (const a of state.assignments) {
|
|
185
|
+
const r = resourceById.get(a.resourceId);
|
|
186
|
+
if (!r) continue;
|
|
187
|
+
if (!isAvailable(r, { start: a.start, end: a.end }, rules)) {
|
|
188
|
+
pushConflict(conflicts, "outside-availability", [a.id]);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return [...conflicts.values()];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// src/engines/calendar/CalendarEngine.ts
|
|
196
|
+
var calendarCounter = 0;
|
|
197
|
+
function createCalendar(core, options) {
|
|
198
|
+
const engineId = `c${calendarCounter++}${options?.name ? `-${options.name}` : ""}`;
|
|
199
|
+
const prefix = CALENDAR_PREFIX;
|
|
200
|
+
const resources = /* @__PURE__ */ new Map();
|
|
201
|
+
const assignments = /* @__PURE__ */ new Map();
|
|
202
|
+
let rules = options?.rules ? [...options.rules] : [];
|
|
203
|
+
let scale = options?.scale ?? "day";
|
|
204
|
+
let conflicts = [];
|
|
205
|
+
const localSubscribers = /* @__PURE__ */ new Set();
|
|
206
|
+
function publish() {
|
|
207
|
+
const resList = [...resources.values()];
|
|
208
|
+
const assignList = [...assignments.values()];
|
|
209
|
+
core.batch(() => {
|
|
210
|
+
core.set(`${prefix}${engineId}.resources`, resList);
|
|
211
|
+
core.set(`${prefix}${engineId}.assignments`, assignList);
|
|
212
|
+
core.set(`${prefix}${engineId}.conflicts`, conflicts);
|
|
213
|
+
core.set(`${prefix}${engineId}.view`, { scale });
|
|
214
|
+
});
|
|
215
|
+
for (const cb of localSubscribers) cb();
|
|
216
|
+
}
|
|
217
|
+
function recompute() {
|
|
218
|
+
conflicts = detectConflicts(
|
|
219
|
+
{ resources: [...resources.values()], assignments: [...assignments.values()] },
|
|
220
|
+
{ scale, rules }
|
|
221
|
+
);
|
|
222
|
+
const involved = /* @__PURE__ */ new Set();
|
|
223
|
+
for (const c of conflicts) for (const id of c.assignmentIds) involved.add(id);
|
|
224
|
+
for (const [id, a] of assignments) {
|
|
225
|
+
const isConflicting = involved.has(id);
|
|
226
|
+
if (isConflicting && a.status !== "conflict") {
|
|
227
|
+
assignments.set(id, { ...a, status: "conflict" });
|
|
228
|
+
} else if (!isConflicting && a.status === "conflict") {
|
|
229
|
+
assignments.set(id, { ...a, status: "tentative" });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
function commitAndPublish() {
|
|
234
|
+
recompute();
|
|
235
|
+
publish();
|
|
236
|
+
}
|
|
237
|
+
function subscribe(cb) {
|
|
238
|
+
localSubscribers.add(cb);
|
|
239
|
+
return () => {
|
|
240
|
+
localSubscribers.delete(cb);
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
function addResource(resource) {
|
|
244
|
+
if (resources.has(resource.id)) {
|
|
245
|
+
throw new Error(
|
|
246
|
+
`[skygraph/calendar] resource id "${resource.id}" already exists`
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
resources.set(resource.id, { ...resource });
|
|
250
|
+
commitAndPublish();
|
|
251
|
+
}
|
|
252
|
+
function removeResource(id) {
|
|
253
|
+
if (!resources.has(id)) return;
|
|
254
|
+
resources.delete(id);
|
|
255
|
+
for (const [aid, a] of assignments) {
|
|
256
|
+
if (a.resourceId === id) assignments.delete(aid);
|
|
257
|
+
}
|
|
258
|
+
commitAndPublish();
|
|
259
|
+
}
|
|
260
|
+
function updateResource(id, patch) {
|
|
261
|
+
const r = resources.get(id);
|
|
262
|
+
if (!r) throw new Error(`[skygraph/calendar] resource "${id}" not found`);
|
|
263
|
+
resources.set(id, { ...r, ...patch, id });
|
|
264
|
+
commitAndPublish();
|
|
265
|
+
}
|
|
266
|
+
function getResource(id) {
|
|
267
|
+
return resources.get(id);
|
|
268
|
+
}
|
|
269
|
+
function getResources() {
|
|
270
|
+
return [...resources.values()];
|
|
271
|
+
}
|
|
272
|
+
function addAssignment(assignment) {
|
|
273
|
+
if (assignments.has(assignment.id)) {
|
|
274
|
+
throw new Error(
|
|
275
|
+
`[skygraph/calendar] assignment id "${assignment.id}" already exists`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
if (!resources.has(assignment.resourceId)) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
`[skygraph/calendar] addAssignment: resource "${assignment.resourceId}" not found`
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
if (assignment.end <= assignment.start) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
`[skygraph/calendar] addAssignment: end (${assignment.end}) must be greater than start (${assignment.start})`
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
assignments.set(assignment.id, { ...assignment });
|
|
289
|
+
commitAndPublish();
|
|
290
|
+
}
|
|
291
|
+
function updateAssignment(id, patch) {
|
|
292
|
+
const a = assignments.get(id);
|
|
293
|
+
if (!a) throw new Error(`[skygraph/calendar] assignment "${id}" not found`);
|
|
294
|
+
const next = { ...a, ...patch, id };
|
|
295
|
+
if (next.end <= next.start) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`[skygraph/calendar] updateAssignment: end (${next.end}) must be greater than start (${next.start})`
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
if (!resources.has(next.resourceId)) {
|
|
301
|
+
throw new Error(
|
|
302
|
+
`[skygraph/calendar] updateAssignment: resource "${next.resourceId}" not found`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
assignments.set(id, next);
|
|
306
|
+
commitAndPublish();
|
|
307
|
+
}
|
|
308
|
+
function removeAssignment(id) {
|
|
309
|
+
if (!assignments.has(id)) return;
|
|
310
|
+
assignments.delete(id);
|
|
311
|
+
commitAndPublish();
|
|
312
|
+
}
|
|
313
|
+
function moveAssignment(id, deltaMs) {
|
|
314
|
+
const a = assignments.get(id);
|
|
315
|
+
if (!a) throw new Error(`[skygraph/calendar] assignment "${id}" not found`);
|
|
316
|
+
if (deltaMs === 0) return;
|
|
317
|
+
assignments.set(id, { ...a, start: a.start + deltaMs, end: a.end + deltaMs });
|
|
318
|
+
commitAndPublish();
|
|
319
|
+
}
|
|
320
|
+
function resizeAssignment(id, side, deltaMs) {
|
|
321
|
+
const a = assignments.get(id);
|
|
322
|
+
if (!a) throw new Error(`[skygraph/calendar] assignment "${id}" not found`);
|
|
323
|
+
if (deltaMs === 0) return;
|
|
324
|
+
let next;
|
|
325
|
+
if (side === "end") {
|
|
326
|
+
const nextEnd = Math.max(a.start + 1, a.end + deltaMs);
|
|
327
|
+
next = { ...a, end: nextEnd };
|
|
328
|
+
} else {
|
|
329
|
+
const nextStart = Math.min(a.end - 1, a.start + deltaMs);
|
|
330
|
+
next = { ...a, start: nextStart };
|
|
331
|
+
}
|
|
332
|
+
assignments.set(id, next);
|
|
333
|
+
commitAndPublish();
|
|
334
|
+
}
|
|
335
|
+
function getAssignment(id) {
|
|
336
|
+
return assignments.get(id);
|
|
337
|
+
}
|
|
338
|
+
function getAssignments() {
|
|
339
|
+
return [...assignments.values()];
|
|
340
|
+
}
|
|
341
|
+
function setRules(next) {
|
|
342
|
+
rules = [...next];
|
|
343
|
+
commitAndPublish();
|
|
344
|
+
}
|
|
345
|
+
function getRules() {
|
|
346
|
+
return [...rules];
|
|
347
|
+
}
|
|
348
|
+
function getConflicts() {
|
|
349
|
+
return [...conflicts];
|
|
350
|
+
}
|
|
351
|
+
function setScale(next) {
|
|
352
|
+
if (next === scale) return;
|
|
353
|
+
scale = next;
|
|
354
|
+
commitAndPublish();
|
|
355
|
+
}
|
|
356
|
+
function getScale() {
|
|
357
|
+
return scale;
|
|
358
|
+
}
|
|
359
|
+
function getState() {
|
|
360
|
+
return {
|
|
361
|
+
resources: [...resources.values()],
|
|
362
|
+
assignments: [...assignments.values()],
|
|
363
|
+
conflicts: [...conflicts]
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function clear() {
|
|
367
|
+
resources.clear();
|
|
368
|
+
assignments.clear();
|
|
369
|
+
rules = [];
|
|
370
|
+
conflicts = [];
|
|
371
|
+
publish();
|
|
372
|
+
}
|
|
373
|
+
publish();
|
|
374
|
+
return {
|
|
375
|
+
id: engineId,
|
|
376
|
+
addResource,
|
|
377
|
+
removeResource,
|
|
378
|
+
updateResource,
|
|
379
|
+
getResource,
|
|
380
|
+
getResources,
|
|
381
|
+
addAssignment,
|
|
382
|
+
updateAssignment,
|
|
383
|
+
removeAssignment,
|
|
384
|
+
moveAssignment,
|
|
385
|
+
resizeAssignment,
|
|
386
|
+
getAssignment,
|
|
387
|
+
getAssignments,
|
|
388
|
+
setRules,
|
|
389
|
+
getRules,
|
|
390
|
+
getConflicts,
|
|
391
|
+
setScale,
|
|
392
|
+
getScale,
|
|
393
|
+
getState,
|
|
394
|
+
subscribe,
|
|
395
|
+
clear
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
399
|
+
0 && (module.exports = {
|
|
400
|
+
createCalendar,
|
|
401
|
+
detectConflicts,
|
|
402
|
+
isAvailable
|
|
403
|
+
});
|
|
404
|
+
//# sourceMappingURL=calendar.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/engines/calendar/index.ts","../src/engines/namespaces.ts","../src/engines/calendar/availability.ts","../src/engines/calendar/conflicts.ts","../src/engines/calendar/CalendarEngine.ts"],"sourcesContent":["export { createCalendar } from './CalendarEngine'\nexport { detectConflicts } from './conflicts'\nexport { isAvailable } from './availability'\nexport type {\n Assignment,\n AssignmentStatus,\n AvailabilityRule,\n CalendarEngine,\n CalendarEngineOptions,\n CalendarScale,\n CalendarState,\n Conflict,\n ConflictReason,\n DayOfWeek,\n DetectConflictsOptions,\n Resource,\n} from './types'\n","/**\n * Engine path namespace registry\n *\n * Every engine that stores state in the Core store under a `$`-prefixed\n * namespace MUST register its prefix here. This prevents two engines from\n * silently colliding on the same root path (e.g. two engines both writing\n * under `$meta.`).\n *\n * Rules:\n * 1. Prefixes MUST start with `$` and end with `.`.\n * 2. Prefixes MUST be unique across all engines.\n * 3. An engine MUST use `reservedPrefix(name)` at module load so\n * collisions surface at import time during dev/test.\n * 4. User data MUST NOT live under `$` prefixes — reserve them for engines.\n */\n\nconst reserved = new Map<string, string>()\n\n/**\n * Reserve (or look up) a prefix for an engine. Throws synchronously if the\n * same prefix is already owned by a different engine.\n */\nexport function reservePrefix(engine: string, prefix: string): string {\n if (!prefix.startsWith('$') || !prefix.endsWith('.')) {\n throw new Error(\n `[skygraph] engine \"${engine}\" tried to reserve invalid prefix \"${prefix}\". ` +\n `Engine prefixes must start with \"$\" and end with \".\".`,\n )\n }\n\n const existing = reserved.get(prefix)\n if (existing !== undefined && existing !== engine) {\n throw new Error(\n `[skygraph] prefix \"${prefix}\" is already owned by engine \"${existing}\", ` +\n `cannot reserve it for \"${engine}\". ` +\n `Pick a different prefix or update the registry.`,\n )\n }\n\n reserved.set(prefix, engine)\n return prefix\n}\n\n/** Snapshot of registered engine prefixes (debug / introspection). */\nexport function listReservedPrefixes(): ReadonlyMap<string, string> {\n return new Map(reserved)\n}\n\n/** Test-only helper. DO NOT call from engine code. */\nexport function __resetPrefixes(): void {\n reserved.clear()\n}\n\n// ── Canonical in-tree reservations ─────────────────────────────────────────\n//\n// Listed here so they are reserved even if the engine module is not loaded.\n// New engines must add their entry here and reference it from their\n// implementation via `reservePrefix(...)`.\n\nexport const FORM_META_PREFIX = reservePrefix('form', '$meta.')\nexport const TABLE_PREFIX = reservePrefix('table', '$table.')\nexport const TREE_PREFIX = reservePrefix('tree', '$tree.')\nexport const GRAPH_PREFIX = reservePrefix('graph', '$graph.')\nexport const CALENDAR_PREFIX = reservePrefix('calendar', '$calendar.')\n","import type { AvailabilityRule, DayOfWeek, Resource } from './types'\n\nconst DAY_MS = 86_400_000\n\n/**\n * Parse `HH:MM` into milliseconds-from-midnight.\n *\n * Returns `null` for malformed input — callers treat that as \"rule has no\n * window on this side\" (i.e. the canonical 00:00 / 24:00 fallback).\n */\nfunction parseHHMM(s: string | undefined, fallback: number): number {\n if (s === undefined) return fallback\n const m = /^(\\d{1,2}):(\\d{2})$/.exec(s)\n if (!m) return fallback\n const h = Number(m[1])\n const mm = Number(m[2])\n if (!Number.isFinite(h) || !Number.isFinite(mm)) return fallback\n if (h < 0 || h > 24 || mm < 0 || mm >= 60) return fallback\n return h * 3_600_000 + mm * 60_000\n}\n\n/** Rules attached to one resource. Cheap to compute, cached per call. */\nfunction rulesFor(\n resourceId: string,\n rules: readonly AvailabilityRule[],\n): AvailabilityRule[] {\n return rules.filter((r) => r.resourceId === resourceId)\n}\n\nfunction dayBucket(t: number): { dayStart: number; dayOfWeek: DayOfWeek } {\n // Floor to UTC day boundary; getUTCDay returns 0..6 (Sun..Sat).\n const d = new Date(t)\n const dayStart =\n Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())\n const dayOfWeek = new Date(dayStart).getUTCDay() as DayOfWeek\n return { dayStart, dayOfWeek }\n}\n\n/**\n * `true` ⇔ the entire `[start, end)` interval falls inside at least one\n * availability window for `resource`. A resource without any matching\n * rule (or without any rules at all) is considered always available —\n * unconfigured calendars stay friendly.\n *\n * Implementation: walk one UTC day at a time and check that the slice of\n * `[start, end)` intersected with that day is fully covered by the union\n * of windows attached to that day. The union is built by sorting rule\n * windows by `from` and merging overlaps in O(k log k) where k = rules\n * for that day (typically 1–3).\n */\nexport function isAvailable(\n resource: Resource,\n range: { start: number; end: number },\n rules: readonly AvailabilityRule[],\n): boolean {\n if (range.end <= range.start) return true\n\n const own = rulesFor(resource.id, rules)\n if (own.length === 0) return true\n\n let cursor = range.start\n while (cursor < range.end) {\n const { dayStart, dayOfWeek } = dayBucket(cursor)\n const dayEnd = dayStart + DAY_MS\n const sliceEnd = Math.min(range.end, dayEnd)\n\n const dayRules = own.filter(\n (r) => r.dayOfWeek === undefined || r.dayOfWeek === dayOfWeek,\n )\n if (dayRules.length === 0) return false\n\n // Build union of windows for this day, in absolute ms.\n const intervals = dayRules\n .map((r) => {\n const fromMs = parseHHMM(r.from, 0)\n const toMs = parseHHMM(r.to, DAY_MS)\n return { from: dayStart + fromMs, to: dayStart + toMs }\n })\n .filter((w) => w.to > w.from)\n .sort((a, b) => a.from - b.from)\n\n if (intervals.length === 0) return false\n\n const merged: Array<{ from: number; to: number }> = []\n for (const w of intervals) {\n const last = merged[merged.length - 1]\n if (last && w.from <= last.to) {\n last.to = Math.max(last.to, w.to)\n } else {\n merged.push({ ...w })\n }\n }\n\n // The required slice for this day is [cursor, sliceEnd). It must be\n // fully contained in the union. We sweep the merged windows.\n let needed = cursor\n for (const w of merged) {\n if (w.from > needed) return false\n if (w.to >= sliceEnd) {\n needed = sliceEnd\n break\n }\n needed = Math.max(needed, w.to)\n }\n if (needed < sliceEnd) return false\n\n cursor = sliceEnd\n }\n\n return true\n}\n","import { isAvailable } from './availability'\nimport type {\n Assignment,\n AvailabilityRule,\n CalendarScale,\n CalendarState,\n Conflict,\n Resource,\n} from './types'\n\nconst DAY_MS = 86_400_000\nconst STEP_MS: Record<CalendarScale, number> = {\n day: DAY_MS,\n week: 7 * DAY_MS,\n month: 30 * DAY_MS,\n}\n\nexport interface DetectConflictsArgs {\n scale?: CalendarScale\n rules?: readonly AvailabilityRule[]\n}\n\nfunction makeId(reason: Conflict['reason'], ids: readonly string[]): string {\n return `${reason}:${[...ids].sort().join(',')}`\n}\n\nfunction pushConflict(\n out: Map<string, Conflict>,\n reason: Conflict['reason'],\n ids: string[],\n): void {\n if (ids.length === 0) return\n const sorted = [...ids].sort()\n const id = makeId(reason, sorted)\n if (out.has(id)) return\n out.set(id, { id, reason, assignmentIds: sorted })\n}\n\n/**\n * Find all conflicts in `state`.\n *\n * Algorithm — three independent passes, results de-duplicated by\n * `(reason, sorted-ids)` so the same overlapping pair is never reported\n * twice as `overlap`:\n *\n * 1. **overlap** (per resource): sort assignments by `start` and sweep.\n * Keep an active set ordered by `end` ASC; whenever a new\n * assignment opens, every still-active assignment forms a pairwise\n * `overlap` conflict with it. O(n log n + k) where k = total\n * overlapping pairs.\n *\n * 2. **over-capacity** (per resource with `capacityPerSlot`): bucket\n * every assignment into the slots it touches (1 slot = 1 scale\n * step). For each slot whose concurrent count exceeds capacity,\n * emit one `over-capacity` conflict naming exactly the assignments\n * that touch that slot. This is the simple counter approach (no\n * interval tree) — fine up to ~10⁵ assignments per resource.\n *\n * 3. **outside-availability**: per-assignment check via\n * `isAvailable`. Each violation becomes its own one-element\n * conflict.\n */\nexport function detectConflicts(\n state: Pick<CalendarState, 'resources' | 'assignments'>,\n opts: DetectConflictsArgs = {},\n): Conflict[] {\n const scale = opts.scale ?? 'day'\n const step = STEP_MS[scale]\n const rules = opts.rules ?? []\n const conflicts = new Map<string, Conflict>()\n\n const byResource = new Map<string, Assignment[]>()\n for (const a of state.assignments) {\n let bucket = byResource.get(a.resourceId)\n if (!bucket) {\n bucket = []\n byResource.set(a.resourceId, bucket)\n }\n bucket.push(a)\n }\n\n // ── 1. Pairwise overlap (per resource) ─────────────────────────────\n for (const list of byResource.values()) {\n const sorted = [...list].sort((x, y) => x.start - y.start)\n for (let i = 0; i < sorted.length; i++) {\n const a = sorted[i]!\n for (let j = i + 1; j < sorted.length; j++) {\n const b = sorted[j]!\n if (b.start >= a.end) break\n // Touching boundaries (b.start === a.end) is NOT a conflict —\n // assignment ranges are half-open [start, end).\n pushConflict(conflicts, 'overlap', [a.id, b.id])\n }\n }\n }\n\n // ── 2. Over-capacity per slot (per resource with capacityPerSlot) ──\n const resourceById = new Map<string, Resource>()\n for (const r of state.resources) resourceById.set(r.id, r)\n\n for (const [rid, list] of byResource) {\n const resource = resourceById.get(rid)\n if (!resource || resource.capacityPerSlot === undefined) continue\n const cap = Math.max(0, Math.floor(resource.capacityPerSlot))\n\n const slotMap = new Map<number, string[]>()\n for (const a of list) {\n // Every slot the assignment touches.\n const lo = Math.floor(a.start / step) * step\n const hi = Math.ceil(a.end / step) * step\n for (let s = lo; s < hi; s += step) {\n let bucket = slotMap.get(s)\n if (!bucket) {\n bucket = []\n slotMap.set(s, bucket)\n }\n bucket.push(a.id)\n }\n }\n\n for (const ids of slotMap.values()) {\n if (ids.length > cap) {\n pushConflict(conflicts, 'over-capacity', ids)\n }\n }\n }\n\n // ── 3. Outside availability (per assignment) ───────────────────────\n if (rules.length > 0) {\n for (const a of state.assignments) {\n const r = resourceById.get(a.resourceId)\n if (!r) continue\n if (!isAvailable(r, { start: a.start, end: a.end }, rules)) {\n pushConflict(conflicts, 'outside-availability', [a.id])\n }\n }\n }\n\n return [...conflicts.values()]\n}\n","import type { Core } from '../../types'\nimport { CALENDAR_PREFIX } from '../namespaces'\nimport { detectConflicts } from './conflicts'\nimport type {\n Assignment,\n AvailabilityRule,\n CalendarEngine,\n CalendarEngineOptions,\n CalendarScale,\n CalendarState,\n Conflict,\n Resource,\n} from './types'\n\nlet calendarCounter = 0\n\n/**\n * Resource Calendar engine — the sixth member of the\n * form / table / tree / virtual / graph / **calendar** family.\n *\n * The in-memory maps are the source of truth and the public read API;\n * snapshots are mirrored to the Core store under\n * `$calendar.<id>.{resources,assignments,conflicts,view}` so other\n * engines / external observers can react via `core.subscribe(...)`.\n *\n * v0 scope:\n * - addResource / removeResource / updateResource\n * - addAssignment / updateAssignment / removeAssignment\n * - moveAssignment(id, deltaMs) — shifts both ends together\n * - resizeAssignment(id, side, deltaMs) — clamps so width >= 1ms\n * - setRules / setScale — re-compute conflicts on change\n * - detectConflicts under the hood; status of involved assignments\n * is auto-promoted to `'conflict'` so the React adapter can render\n * without re-running the algorithm.\n *\n * NOT in v0 (deferred to round-10):\n * - exception dates on `AvailabilityRule`\n * - full per-engine undo/redo (use `core.snapshot/restore` instead)\n * - aggregate read APIs over a sub-range\n */\nexport function createCalendar(\n core: Core,\n options?: CalendarEngineOptions,\n): CalendarEngine {\n const engineId = `c${calendarCounter++}${options?.name ? `-${options.name}` : ''}`\n const prefix = CALENDAR_PREFIX\n\n const resources = new Map<string, Resource>()\n const assignments = new Map<string, Assignment>()\n let rules: AvailabilityRule[] = options?.rules ? [...options.rules] : []\n let scale: CalendarScale = options?.scale ?? 'day'\n let conflicts: Conflict[] = []\n\n const localSubscribers = new Set<() => void>()\n\n function publish(): void {\n const resList = [...resources.values()]\n const assignList = [...assignments.values()]\n core.batch(() => {\n core.set(`${prefix}${engineId}.resources`, resList)\n core.set(`${prefix}${engineId}.assignments`, assignList)\n core.set(`${prefix}${engineId}.conflicts`, conflicts)\n core.set(`${prefix}${engineId}.view`, { scale })\n })\n for (const cb of localSubscribers) cb()\n }\n\n function recompute(): void {\n conflicts = detectConflicts(\n { resources: [...resources.values()], assignments: [...assignments.values()] },\n { scale, rules },\n )\n // Promote involved assignments to status: 'conflict'. Assignments\n // that aren't in any conflict get their original (or default)\n // status restored. We don't lose the underlying intent: the\n // promotion is reversible and recomputed on every mutation.\n const involved = new Set<string>()\n for (const c of conflicts) for (const id of c.assignmentIds) involved.add(id)\n for (const [id, a] of assignments) {\n const isConflicting = involved.has(id)\n if (isConflicting && a.status !== 'conflict') {\n assignments.set(id, { ...a, status: 'conflict' })\n } else if (!isConflicting && a.status === 'conflict') {\n // Drop the auto-`conflict` flag — the original intent (tentative\n // / confirmed / undefined) was lost when we promoted, and the\n // adapter only needs to know \"no longer conflicting\", so we\n // default back to `'tentative'` which is the safest fallback.\n assignments.set(id, { ...a, status: 'tentative' })\n }\n }\n }\n\n function commitAndPublish(): void {\n recompute()\n publish()\n }\n\n function subscribe(cb: () => void): () => void {\n localSubscribers.add(cb)\n return () => {\n localSubscribers.delete(cb)\n }\n }\n\n function addResource(resource: Resource): void {\n if (resources.has(resource.id)) {\n throw new Error(\n `[skygraph/calendar] resource id \"${resource.id}\" already exists`,\n )\n }\n resources.set(resource.id, { ...resource })\n commitAndPublish()\n }\n\n function removeResource(id: string): void {\n if (!resources.has(id)) return\n resources.delete(id)\n // Cascade: drop assignments tied to this resource.\n for (const [aid, a] of assignments) {\n if (a.resourceId === id) assignments.delete(aid)\n }\n commitAndPublish()\n }\n\n function updateResource(id: string, patch: Partial<Resource>): void {\n const r = resources.get(id)\n if (!r) throw new Error(`[skygraph/calendar] resource \"${id}\" not found`)\n resources.set(id, { ...r, ...patch, id })\n commitAndPublish()\n }\n\n function getResource(id: string): Resource | undefined {\n return resources.get(id)\n }\n\n function getResources(): Resource[] {\n return [...resources.values()]\n }\n\n function addAssignment(assignment: Assignment): void {\n if (assignments.has(assignment.id)) {\n throw new Error(\n `[skygraph/calendar] assignment id \"${assignment.id}\" already exists`,\n )\n }\n if (!resources.has(assignment.resourceId)) {\n throw new Error(\n `[skygraph/calendar] addAssignment: resource \"${assignment.resourceId}\" not found`,\n )\n }\n if (assignment.end <= assignment.start) {\n throw new Error(\n `[skygraph/calendar] addAssignment: end (${assignment.end}) must be greater than start (${assignment.start})`,\n )\n }\n assignments.set(assignment.id, { ...assignment })\n commitAndPublish()\n }\n\n function updateAssignment(id: string, patch: Partial<Assignment>): void {\n const a = assignments.get(id)\n if (!a) throw new Error(`[skygraph/calendar] assignment \"${id}\" not found`)\n const next: Assignment = { ...a, ...patch, id }\n if (next.end <= next.start) {\n throw new Error(\n `[skygraph/calendar] updateAssignment: end (${next.end}) must be greater than start (${next.start})`,\n )\n }\n if (!resources.has(next.resourceId)) {\n throw new Error(\n `[skygraph/calendar] updateAssignment: resource \"${next.resourceId}\" not found`,\n )\n }\n assignments.set(id, next)\n commitAndPublish()\n }\n\n function removeAssignment(id: string): void {\n if (!assignments.has(id)) return\n assignments.delete(id)\n commitAndPublish()\n }\n\n function moveAssignment(id: string, deltaMs: number): void {\n const a = assignments.get(id)\n if (!a) throw new Error(`[skygraph/calendar] assignment \"${id}\" not found`)\n if (deltaMs === 0) return\n assignments.set(id, { ...a, start: a.start + deltaMs, end: a.end + deltaMs })\n commitAndPublish()\n }\n\n function resizeAssignment(\n id: string,\n side: 'start' | 'end',\n deltaMs: number,\n ): void {\n const a = assignments.get(id)\n if (!a) throw new Error(`[skygraph/calendar] assignment \"${id}\" not found`)\n if (deltaMs === 0) return\n let next: Assignment\n if (side === 'end') {\n const nextEnd = Math.max(a.start + 1, a.end + deltaMs)\n next = { ...a, end: nextEnd }\n } else {\n const nextStart = Math.min(a.end - 1, a.start + deltaMs)\n next = { ...a, start: nextStart }\n }\n assignments.set(id, next)\n commitAndPublish()\n }\n\n function getAssignment(id: string): Assignment | undefined {\n return assignments.get(id)\n }\n\n function getAssignments(): Assignment[] {\n return [...assignments.values()]\n }\n\n function setRules(next: readonly AvailabilityRule[]): void {\n rules = [...next]\n commitAndPublish()\n }\n\n function getRules(): AvailabilityRule[] {\n return [...rules]\n }\n\n function getConflicts(): Conflict[] {\n return [...conflicts]\n }\n\n function setScale(next: CalendarScale): void {\n if (next === scale) return\n scale = next\n commitAndPublish()\n }\n\n function getScale(): CalendarScale {\n return scale\n }\n\n function getState(): CalendarState {\n return {\n resources: [...resources.values()],\n assignments: [...assignments.values()],\n conflicts: [...conflicts],\n }\n }\n\n function clear(): void {\n resources.clear()\n assignments.clear()\n rules = []\n conflicts = []\n publish()\n }\n\n publish() // initial empty publish so subscribers see a stable shape\n\n return {\n id: engineId,\n addResource,\n removeResource,\n updateResource,\n getResource,\n getResources,\n addAssignment,\n updateAssignment,\n removeAssignment,\n moveAssignment,\n resizeAssignment,\n getAssignment,\n getAssignments,\n setRules,\n getRules,\n getConflicts,\n setScale,\n getScale,\n getState,\n subscribe,\n clear,\n }\n}\n"],"mappings":"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","names":["DAY_MS"]}
|