@futdevpro/fsm-dynamo 1.20.95 → 1.20.99
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/build/_modules/game/_collections/build-reservation.util.d.ts +31 -0
- package/build/_modules/game/_collections/build-reservation.util.d.ts.map +1 -0
- package/build/_modules/game/_collections/build-reservation.util.js +77 -0
- package/build/_modules/game/_collections/build-reservation.util.js.map +1 -0
- package/build/_modules/game/_collections/game-save.util.d.ts +46 -0
- package/build/_modules/game/_collections/game-save.util.d.ts.map +1 -0
- package/build/_modules/game/_collections/game-save.util.js +189 -0
- package/build/_modules/game/_collections/game-save.util.js.map +1 -0
- package/build/_modules/game/_models/build-reservation.interface.d.ts +70 -0
- package/build/_modules/game/_models/build-reservation.interface.d.ts.map +1 -0
- package/build/_modules/game/_models/build-reservation.interface.js +7 -0
- package/build/_modules/game/_models/build-reservation.interface.js.map +1 -0
- package/build/_modules/game/_models/flying-effect.control-model.d.ts +7 -0
- package/build/_modules/game/_models/flying-effect.control-model.d.ts.map +1 -1
- package/build/_modules/game/_models/flying-effect.control-model.js +12 -1
- package/build/_modules/game/_models/flying-effect.control-model.js.map +1 -1
- package/build/_modules/game/_models/flying-effect.interface.d.ts +3 -0
- package/build/_modules/game/_models/flying-effect.interface.d.ts.map +1 -1
- package/build/_modules/game/_models/game-save.interface.d.ts +76 -0
- package/build/_modules/game/_models/game-save.interface.d.ts.map +1 -0
- package/build/_modules/game/_models/game-save.interface.js +7 -0
- package/build/_modules/game/_models/game-save.interface.js.map +1 -0
- package/build/_modules/game/_models/simulation-clock.control-model.d.ts +39 -0
- package/build/_modules/game/_models/simulation-clock.control-model.d.ts.map +1 -0
- package/build/_modules/game/_models/simulation-clock.control-model.js +122 -0
- package/build/_modules/game/_models/simulation-clock.control-model.js.map +1 -0
- package/build/_modules/game/_models/simulation-clock.interface.d.ts +44 -0
- package/build/_modules/game/_models/simulation-clock.interface.d.ts.map +1 -0
- package/build/_modules/game/_models/simulation-clock.interface.js +3 -0
- package/build/_modules/game/_models/simulation-clock.interface.js.map +1 -0
- package/build/_modules/game/_models/work-order-scheduler.control-model.d.ts +55 -0
- package/build/_modules/game/_models/work-order-scheduler.control-model.d.ts.map +1 -0
- package/build/_modules/game/_models/work-order-scheduler.control-model.js +201 -0
- package/build/_modules/game/_models/work-order-scheduler.control-model.js.map +1 -0
- package/build/_modules/game/_models/work-order-scheduler.interface.d.ts +43 -0
- package/build/_modules/game/_models/work-order-scheduler.interface.d.ts.map +1 -0
- package/build/_modules/game/_models/work-order-scheduler.interface.js +3 -0
- package/build/_modules/game/_models/work-order-scheduler.interface.js.map +1 -0
- package/build/_modules/game/index.d.ts +8 -0
- package/build/_modules/game/index.d.ts.map +1 -1
- package/build/_modules/game/index.js +8 -0
- package/build/_modules/game/index.js.map +1 -1
- package/build-esm/_modules/game/_collections/build-reservation.util.js +72 -0
- package/build-esm/_modules/game/_collections/game-save.util.js +184 -0
- package/build-esm/_modules/game/_models/build-reservation.interface.js +5 -0
- package/build-esm/_modules/game/_models/flying-effect.control-model.js +12 -1
- package/build-esm/_modules/game/_models/game-save.interface.js +5 -0
- package/build-esm/_modules/game/_models/simulation-clock.control-model.js +117 -0
- package/build-esm/_modules/game/_models/simulation-clock.interface.js +1 -0
- package/build-esm/_modules/game/_models/work-order-scheduler.control-model.js +196 -0
- package/build-esm/_modules/game/_models/work-order-scheduler.interface.js +1 -0
- package/build-esm/_modules/game/index.js +8 -0
- package/package.json +1 -1
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { DyFM_Error } from '../../../_models/control-models/error.control-model';
|
|
2
|
+
/**
|
|
3
|
+
* BFR-WARFACTORY-012 — headless, cancel-safe scheduler of interchangeable workers over parallel work orders.
|
|
4
|
+
*
|
|
5
|
+
* Guarantees (each pinned by a spec):
|
|
6
|
+
* - **Release is structural, not a happy-path step:** an assignment exists only as `worker → order`; cancelling an
|
|
7
|
+
* order, completing it or removing (destroying) a worker deletes the entry in the same call ⇒ no stranded worker.
|
|
8
|
+
* - **Every pending order gets ≥ 1 worker when possible:** idle workers first; if none is idle, one is moved from the
|
|
9
|
+
* order that has the most (only from an order with > 1) — so one plan cannot starve the others ("frozen" plans).
|
|
10
|
+
* - **Stable least-assigned distribution:** extra workers go to the order with the fewest (tie → the earliest order);
|
|
11
|
+
* existing assignments are not reshuffled.
|
|
12
|
+
* - **Self-healing import:** stale ownership (unknown worker / order, duplicates) is dropped and REPORTED; importing
|
|
13
|
+
* the same state twice gives the same result.
|
|
14
|
+
* No clock, no RNG: time comes in through `advance(simulationSeconds)` (e.g. from `DyFM_SimulationClock_ControlModel`).
|
|
15
|
+
*/
|
|
16
|
+
export class DyFM_WorkOrderScheduler_ControlModel {
|
|
17
|
+
rate;
|
|
18
|
+
orders = [];
|
|
19
|
+
workerIds = [];
|
|
20
|
+
/** workerId → orderId (the ONLY ownership record). */
|
|
21
|
+
assignments = new Map();
|
|
22
|
+
/** Creates the scheduler; an invalid rate throws `DYFM-WORK-ORDER-CONFIG`. */
|
|
23
|
+
constructor(config = {}) {
|
|
24
|
+
const rate = config.workPerWorkerPerSecond ?? 1;
|
|
25
|
+
if (!Number.isFinite(rate) || rate <= 0) {
|
|
26
|
+
throw DyFM_WorkOrderScheduler_ControlModel.error('DYFM-WORK-ORDER-CONFIG', `workPerWorkerPerSecond must be > 0 (${rate})`);
|
|
27
|
+
}
|
|
28
|
+
this.rate = rate;
|
|
29
|
+
}
|
|
30
|
+
/** Adds a work order at the end of the FIFO; a duplicate id or non-positive work throws `DYFM-WORK-ORDER-INVALID`. */
|
|
31
|
+
addOrder(order) {
|
|
32
|
+
if (!order?.orderId || this.orders.some((o) => o.orderId === order.orderId)
|
|
33
|
+
|| !Number.isFinite(order.work) || order.work <= 0) {
|
|
34
|
+
throw DyFM_WorkOrderScheduler_ControlModel.error('DYFM-WORK-ORDER-INVALID', `order needs a unique orderId and work > 0 (${order?.orderId})`);
|
|
35
|
+
}
|
|
36
|
+
this.orders.push({ orderId: order.orderId, work: order.work, progress: Math.max(0, order.progress ?? 0) });
|
|
37
|
+
this.rebalance();
|
|
38
|
+
}
|
|
39
|
+
/** Adds an (idle) worker; duplicates are ignored. */
|
|
40
|
+
addWorker(workerId) {
|
|
41
|
+
if (workerId && !this.workerIds.includes(workerId)) {
|
|
42
|
+
this.workerIds.push(workerId);
|
|
43
|
+
this.rebalance();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Removes (destroys) a worker; its assignment goes with it. Idempotent. */
|
|
47
|
+
removeWorker(workerId) {
|
|
48
|
+
this.workerIds = this.workerIds.filter((id) => id !== workerId);
|
|
49
|
+
this.assignments.delete(workerId);
|
|
50
|
+
this.rebalance();
|
|
51
|
+
}
|
|
52
|
+
/** Cancels an order: removes it and releases its workers in the same call. Idempotent (unknown → found: false). */
|
|
53
|
+
cancel(orderId) {
|
|
54
|
+
const order = this.orders.find((o) => o.orderId === orderId);
|
|
55
|
+
if (!order) {
|
|
56
|
+
return { found: false, releasedWorkerIds: [], progressRatio: 0 };
|
|
57
|
+
}
|
|
58
|
+
const released = this.workersOf(orderId);
|
|
59
|
+
this.orders = this.orders.filter((o) => o.orderId !== orderId);
|
|
60
|
+
released.forEach((workerId) => this.assignments.delete(workerId));
|
|
61
|
+
this.rebalance();
|
|
62
|
+
return { found: true, releasedWorkerIds: released, progressRatio: Math.min(1, order.progress / order.work) };
|
|
63
|
+
}
|
|
64
|
+
/** Advances every order by its workers × rate × seconds; completed orders leave and free their workers. */
|
|
65
|
+
advance(simulationSeconds) {
|
|
66
|
+
if (!Number.isFinite(simulationSeconds) || simulationSeconds <= 0) {
|
|
67
|
+
return { completedOrderIds: [] };
|
|
68
|
+
}
|
|
69
|
+
const completed = [];
|
|
70
|
+
for (const order of this.orders) {
|
|
71
|
+
order.progress += this.workersOf(order.orderId).length * this.rate * simulationSeconds;
|
|
72
|
+
if (order.progress >= order.work) {
|
|
73
|
+
completed.push(order.orderId);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (completed.length) {
|
|
77
|
+
this.orders = this.orders.filter((o) => !completed.includes(o.orderId));
|
|
78
|
+
for (const [workerId, orderId] of [...this.assignments]) {
|
|
79
|
+
if (completed.includes(orderId)) {
|
|
80
|
+
this.assignments.delete(workerId);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
this.rebalance();
|
|
84
|
+
}
|
|
85
|
+
return { completedOrderIds: completed };
|
|
86
|
+
}
|
|
87
|
+
/** The workers of an order, in assignment order. */
|
|
88
|
+
workersOf(orderId) {
|
|
89
|
+
return [...this.assignments].filter(([, id]) => id === orderId)
|
|
90
|
+
.map(([workerId]) => workerId);
|
|
91
|
+
}
|
|
92
|
+
/** The order a worker is on (`undefined` = idle). */
|
|
93
|
+
orderOf(workerId) {
|
|
94
|
+
return this.assignments.get(workerId);
|
|
95
|
+
}
|
|
96
|
+
/** Current orders (copies, FIFO). */
|
|
97
|
+
getOrders() {
|
|
98
|
+
return this.orders.map((o) => ({ ...o }));
|
|
99
|
+
}
|
|
100
|
+
/** The save/load snapshot. */
|
|
101
|
+
exportState() {
|
|
102
|
+
return {
|
|
103
|
+
schemaVersion: 1,
|
|
104
|
+
orders: this.getOrders(),
|
|
105
|
+
workerIds: [...this.workerIds],
|
|
106
|
+
assignments: Object.fromEntries(this.assignments),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Restores a snapshot and HEALS stale ownership (assignments to unknown workers / orders, duplicates): dropped and
|
|
111
|
+
* reported, then the distribution rules re-apply. An unreadable snapshot throws `DYFM-WORK-ORDER-STATE`.
|
|
112
|
+
*/
|
|
113
|
+
importState(state) {
|
|
114
|
+
if (!state || state.schemaVersion !== 1 || !Array.isArray(state.orders) || !Array.isArray(state.workerIds)
|
|
115
|
+
|| !state.assignments || typeof state.assignments !== 'object') {
|
|
116
|
+
throw DyFM_WorkOrderScheduler_ControlModel.error('DYFM-WORK-ORDER-STATE', 'unreadable scheduler snapshot');
|
|
117
|
+
}
|
|
118
|
+
const report = { droppedUnknownWorkers: [], droppedUnknownOrders: [], skippedDuplicates: [] };
|
|
119
|
+
const orders = [];
|
|
120
|
+
for (const order of state.orders) {
|
|
121
|
+
if (!order?.orderId || orders.some((o) => o.orderId === order.orderId)
|
|
122
|
+
|| !Number.isFinite(order.work) || order.work <= 0) {
|
|
123
|
+
report.skippedDuplicates.push(String(order?.orderId));
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (Number(order.progress) >= order.work) {
|
|
127
|
+
continue; // a completed order is not "active" — its assignments heal below
|
|
128
|
+
}
|
|
129
|
+
orders.push({ orderId: order.orderId, work: order.work, progress: Math.max(0, Number(order.progress) || 0) });
|
|
130
|
+
}
|
|
131
|
+
const workerIds = [];
|
|
132
|
+
for (const workerId of state.workerIds) {
|
|
133
|
+
if (!workerId || workerIds.includes(workerId)) {
|
|
134
|
+
report.skippedDuplicates.push(String(workerId));
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
workerIds.push(workerId);
|
|
138
|
+
}
|
|
139
|
+
const assignments = new Map();
|
|
140
|
+
for (const [workerId, orderId] of Object.entries(state.assignments)) {
|
|
141
|
+
if (!workerIds.includes(workerId)) {
|
|
142
|
+
report.droppedUnknownWorkers.push(workerId);
|
|
143
|
+
}
|
|
144
|
+
else if (!orders.some((o) => o.orderId === orderId)) {
|
|
145
|
+
report.droppedUnknownOrders.push(`${workerId}→${orderId}`);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
assignments.set(workerId, orderId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
this.orders = orders;
|
|
152
|
+
this.workerIds = workerIds;
|
|
153
|
+
this.assignments = assignments;
|
|
154
|
+
this.rebalance();
|
|
155
|
+
return report;
|
|
156
|
+
}
|
|
157
|
+
/** Applies the distribution rules (stable: existing valid assignments are kept). */
|
|
158
|
+
rebalance() {
|
|
159
|
+
const count = (orderId) => this.workersOf(orderId).length;
|
|
160
|
+
const idle = () => this.workerIds.filter((id) => !this.assignments.has(id));
|
|
161
|
+
// 1) every order without a worker gets one: an idle worker, else one moved from the richest order (> 1)
|
|
162
|
+
for (const order of this.orders) {
|
|
163
|
+
if (count(order.orderId) > 0) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const free = idle()[0];
|
|
167
|
+
if (free) {
|
|
168
|
+
this.assignments.set(free, order.orderId);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
const donor = [...this.orders]
|
|
172
|
+
.filter((o) => count(o.orderId) > 1)
|
|
173
|
+
.sort((a, b) => count(b.orderId) - count(a.orderId) || this.orders.indexOf(b) - this.orders.indexOf(a))[0];
|
|
174
|
+
if (donor) {
|
|
175
|
+
const moved = this.workersOf(donor.orderId).slice(-1)[0];
|
|
176
|
+
this.assignments.set(moved, order.orderId);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// 2) the remaining idle workers go to the least-assigned order (tie → the earliest)
|
|
180
|
+
for (const workerId of idle()) {
|
|
181
|
+
const target = [...this.orders]
|
|
182
|
+
.sort((a, b) => count(a.orderId) - count(b.orderId) || this.orders.indexOf(a) - this.orders.indexOf(b))[0];
|
|
183
|
+
if (!target) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
this.assignments.set(workerId, target.orderId);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
static error(code, message) {
|
|
190
|
+
return new DyFM_Error({
|
|
191
|
+
message: `DyFM_WorkOrderScheduler: ${message}`,
|
|
192
|
+
errorCode: code,
|
|
193
|
+
issuerService: 'DyFM_WorkOrderScheduler_ControlModel',
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -12,6 +12,14 @@ export * from './_models/seeded-random-registry.interface';
|
|
|
12
12
|
export * from './_models/seeded-random-registry.control-model';
|
|
13
13
|
export * from './_models/flying-effect.interface';
|
|
14
14
|
export * from './_models/flying-effect.control-model';
|
|
15
|
+
export * from './_models/simulation-clock.interface';
|
|
16
|
+
export * from './_models/simulation-clock.control-model';
|
|
17
|
+
export * from './_models/game-save.interface';
|
|
18
|
+
export * from './_models/work-order-scheduler.interface';
|
|
19
|
+
export * from './_models/work-order-scheduler.control-model';
|
|
20
|
+
export * from './_models/build-reservation.interface';
|
|
15
21
|
// COLLECTIONS
|
|
16
22
|
export * from './_collections/audio-mixer.util';
|
|
17
23
|
export * from './_collections/audio-scale.util';
|
|
24
|
+
export * from './_collections/game-save.util';
|
|
25
|
+
export * from './_collections/build-reservation.util';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@futdevpro/fsm-dynamo",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.99",
|
|
4
4
|
"description": "Full Stack Model Collection for Dynamic (NodeJS-Typescript) Framework called Dynamo, by Future Development Ltd.",
|
|
5
5
|
"DyBu_settings": {
|
|
6
6
|
"packageType": "full-stack-package",
|