@operato/twin-kernel 0.7.10 → 0.7.11
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/dist/contract.d.ts +0 -8
- package/dist/contract.js +90 -6
- package/dist/flow-engine.d.ts +14 -0
- package/dist/flow-engine.js +22 -1
- package/dist-cjs/index.cjs +57 -7
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -336,14 +336,6 @@ export interface ResourceClassDef extends EffectivePeriod {
|
|
|
336
336
|
/** 이 등급의 적격을 정하는 시험 명세들 — 표준 `TestSpecificationID`. */
|
|
337
337
|
testSpecificationIds?: TestSpecificationRefs;
|
|
338
338
|
}
|
|
339
|
-
/**
|
|
340
|
-
* 등급 소속을 **상속을 타고 닫는다** — "이 개체가 이 등급으로 통하는가".
|
|
341
|
-
*
|
|
342
|
-
* 순환은 방문 집합으로 끊는다(잘못된 마스터가 무한 루프를 만들지 않게). 등급 정의가 없으면 소속
|
|
343
|
-
* 그대로만 본다 — 정의를 요구하지 않는다(정의를 싣지 않은 트윈이 그대로 동작해야 한다).
|
|
344
|
-
*
|
|
345
|
-
* `at` 를 주면 **유효 기간 밖의 등급은 제외**한다. 안 주면 기간을 보지 않는다(모르면 판단하지 않는다).
|
|
346
|
-
*/
|
|
347
339
|
export declare function classClosure(directIds: readonly string[] | undefined, defs: readonly ResourceClassDef[] | undefined, at?: ISOTime): Set<string>;
|
|
348
340
|
/**
|
|
349
341
|
* 우선순위 — **ISA-95 `Priority`**(`JobOrderType`·`OperationsRequestType`, 타입은 `PriorityType` =
|
package/dist/contract.js
CHANGED
|
@@ -184,17 +184,42 @@ export function meetsTests(required, results, at) {
|
|
|
184
184
|
export function effectivityAt(p, at) {
|
|
185
185
|
if (!p || !at)
|
|
186
186
|
return undefined;
|
|
187
|
-
const atMs =
|
|
187
|
+
const atMs = parsedMs(at);
|
|
188
188
|
if (!Number.isFinite(atMs))
|
|
189
189
|
return undefined;
|
|
190
|
-
const from = p.effectiveStart ?
|
|
190
|
+
const from = p.effectiveStart ? parsedMs(p.effectiveStart) : NaN;
|
|
191
191
|
if (Number.isFinite(from) && atMs < from)
|
|
192
192
|
return 'not-yet';
|
|
193
|
-
const to = p.effectiveEnd ?
|
|
193
|
+
const to = p.effectiveEnd ? parsedMs(p.effectiveEnd) : NaN;
|
|
194
194
|
if (Number.isFinite(to) && atMs > to)
|
|
195
195
|
return 'expired';
|
|
196
196
|
return undefined;
|
|
197
197
|
}
|
|
198
|
+
/*
|
|
199
|
+
* ── 같은 시각 문자열을 몇천 번 다시 파싱하지 않는다 (2026-08-17 프로파일) ───
|
|
200
|
+
*
|
|
201
|
+
* 커널 틱의 비용을 재 보니 `Date.parse` 를 부르는 이 판정들이 **틱 시간의 5분의 1** 이었다. 한 틱
|
|
202
|
+
* 안에서 오가는 시각 문자열은 사실 몇 개뿐이다: 그 틱의 "지금" 하나와, 마스터에 적힌 유효기간들
|
|
203
|
+
* (부팅 뒤 바뀌지 않는다). 같은 문자열이 계속 다시 파싱되고 있었다.
|
|
204
|
+
*
|
|
205
|
+
* 값이 아니라 **문자열 자체**를 열쇠로 기억한다 — 같은 문자열은 언제 물어도 같은 밀리초다(ISO 시각은
|
|
206
|
+
* 절대 시각이다). 그래서 이 기억은 결과를 바꾸지 않는다: 캐시가 없을 때와 정확히 같은 수를 낸다.
|
|
207
|
+
*
|
|
208
|
+
* 크기를 묶는다. 시각 문자열은 트윈이 굴러가는 동안 계속 새로 생기므로(틱마다 새 "지금"), 묶지 않으면
|
|
209
|
+
* 이 표가 곧 누수다. 실제로 필요한 것은 최근 몇 개뿐이다.
|
|
210
|
+
*/
|
|
211
|
+
const PARSED_MAX = 64;
|
|
212
|
+
const parsedCache = new Map();
|
|
213
|
+
function parsedMs(at) {
|
|
214
|
+
const hit = parsedCache.get(at);
|
|
215
|
+
if (hit !== undefined)
|
|
216
|
+
return hit;
|
|
217
|
+
const ms = Date.parse(at);
|
|
218
|
+
if (parsedCache.size >= PARSED_MAX)
|
|
219
|
+
parsedCache.clear();
|
|
220
|
+
parsedCache.set(at, ms);
|
|
221
|
+
return ms;
|
|
222
|
+
}
|
|
198
223
|
/**
|
|
199
224
|
* 등급 소속을 **상속을 타고 닫는다** — "이 개체가 이 등급으로 통하는가".
|
|
200
225
|
*
|
|
@@ -203,8 +228,30 @@ export function effectivityAt(p, at) {
|
|
|
203
228
|
*
|
|
204
229
|
* `at` 를 주면 **유효 기간 밖의 등급은 제외**한다. 안 주면 기간을 보지 않는다(모르면 판단하지 않는다).
|
|
205
230
|
*/
|
|
231
|
+
/*
|
|
232
|
+
* ── 등급 정의 색인은 한 번만 만든다 (2026-08-17 프로파일) ───────────────────
|
|
233
|
+
*
|
|
234
|
+
* `classClosure` 는 부를 때마다 정의 목록으로 `Map` 을 새로 지었다. 그런데 등급 정의는 모델을 실을 때
|
|
235
|
+
* 정해지고 그 뒤 바뀌지 않는 목록이다 — 능력 판정마다 같은 표를 다시 짓느라 **틱 시간의 7분의 1** 을
|
|
236
|
+
* 썼다(프로파일 `classClosure` 14%).
|
|
237
|
+
*
|
|
238
|
+
* 배열 **자체**를 열쇠로 기억한다(`WeakMap`). 목록이 바뀌면 그것은 다른 배열이므로 새 표가 만들어지고,
|
|
239
|
+
* 목록이 사라지면 표도 함께 사라진다 — 무효화를 따로 관리할 것이 없다.
|
|
240
|
+
*/
|
|
241
|
+
const classIndexCache = new WeakMap();
|
|
242
|
+
function classIndex(defs) {
|
|
243
|
+
if (!defs)
|
|
244
|
+
return EMPTY_CLASS_INDEX;
|
|
245
|
+
const hit = classIndexCache.get(defs);
|
|
246
|
+
if (hit)
|
|
247
|
+
return hit;
|
|
248
|
+
const built = new Map(defs.map(d => [d.id, d]));
|
|
249
|
+
classIndexCache.set(defs, built);
|
|
250
|
+
return built;
|
|
251
|
+
}
|
|
252
|
+
const EMPTY_CLASS_INDEX = new Map();
|
|
206
253
|
export function classClosure(directIds, defs, at) {
|
|
207
|
-
const byId =
|
|
254
|
+
const byId = classIndex(defs);
|
|
208
255
|
const inWindow = (d) => !d || effectivityAt(d, at) === undefined;
|
|
209
256
|
const out = new Set();
|
|
210
257
|
const stack = [...(directIds ?? [])];
|
|
@@ -499,8 +546,17 @@ export function activeShiftAt(entries, atMs, utcOffsetMinutes) {
|
|
|
499
546
|
* 06시 교대가 **7시간 틀렸다.** 선언이 없으면 UTC 이고, 그 기본값을 숨기지 않고 밝힌다.
|
|
500
547
|
*/
|
|
501
548
|
export function minuteOfDayAt(ms, utcOffsetMinutes) {
|
|
502
|
-
|
|
503
|
-
|
|
549
|
+
/*
|
|
550
|
+
* `new Date(...)` 를 만들지 않는다 (2026-08-17 프로파일).
|
|
551
|
+
*
|
|
552
|
+
* 교대 판정이 이것을 자원마다 부르는데, 그때마다 Date 객체를 하나 만들고 버렸다 — 틱 시간의 12% 였다.
|
|
553
|
+
* UTC 기준 「하루 안의 분」은 나눗셈으로 나온다(getUTCHours 도 같은 계산을 한다). 음수 시각(1970 이전)
|
|
554
|
+
* 에서도 같은 답이 되도록 나머지를 한 번 더 올린다.
|
|
555
|
+
*/
|
|
556
|
+
const shifted = ms + (utcOffsetMinutes ?? 0) * 60_000;
|
|
557
|
+
const DAY = 86_400_000;
|
|
558
|
+
const inDay = ((shifted % DAY) + DAY) % DAY;
|
|
559
|
+
return Math.floor(inDay / 60_000);
|
|
504
560
|
}
|
|
505
561
|
export function offCalendarReasonAt(r, ms, utcOffsetMinutes) {
|
|
506
562
|
if (!offCalendarAt(r, ms, utcOffsetMinutes))
|
|
@@ -539,13 +595,41 @@ export function offCalendarAt(r, ms, utcOffsetMinutes) {
|
|
|
539
595
|
export function requiredTestsFor(directIds, defs, at) {
|
|
540
596
|
if (!defs?.length)
|
|
541
597
|
return [];
|
|
598
|
+
/*
|
|
599
|
+
* ── 같은 답을 틱마다 다시 닫지 않는다 (2026-08-17 프로파일) ──────────────
|
|
600
|
+
*
|
|
601
|
+
* 이 함수의 답은 **셋에만** 달려 있다: 소속 등급, 등급 정의, 판정 시각. 자원의 상태(고장·교대·시험
|
|
602
|
+
* 결과)는 여기 들어오지 않는다 — 그것은 `capabilityOf` 가 따로 본다. 그런데 능력 판정이 자원마다·
|
|
603
|
+
* 작업마다 이것을 불러, 같은 셋으로 같은 답을 한 틱에 수백 번 다시 만들었다(닫기 + 정의 순회 =
|
|
604
|
+
* 프로파일 21%).
|
|
605
|
+
*
|
|
606
|
+
* 그래서 그 셋을 열쇠로 기억한다. 정의 목록은 배열 **자체**로 구분하므로(`classIndex` 와 같은 규율)
|
|
607
|
+
* 모델이 바뀌면 다른 열쇠가 된다. 시각이 흐르면 열쇠도 바뀌므로 유효기간 판정이 낡지 않는다.
|
|
608
|
+
*
|
|
609
|
+
* 돌려주는 배열은 **읽기 전용으로 다뤄야 한다** — 기억된 배열을 부르는 쪽이 고치면 다음 호출자가
|
|
610
|
+
* 고쳐진 것을 받는다. 지금 모든 소비처는 읽기만 한다(그래서 복사하지 않는다: 복사하면 이 기억의
|
|
611
|
+
* 값이 절반은 사라진다).
|
|
612
|
+
*/
|
|
613
|
+
const cache = requiredTestsCache.get(defs) ?? new Map();
|
|
614
|
+
if (!requiredTestsCache.has(defs))
|
|
615
|
+
requiredTestsCache.set(defs, cache);
|
|
616
|
+
const key = `${at ?? ''}\u0000${(directIds ?? []).join('\u0001')}`;
|
|
617
|
+
const hit = cache.get(key);
|
|
618
|
+
if (hit)
|
|
619
|
+
return hit;
|
|
542
620
|
const closure = classClosure(directIds, defs, at);
|
|
543
621
|
const required = [];
|
|
544
622
|
for (const d of defs)
|
|
545
623
|
if (closure.has(d.id))
|
|
546
624
|
required.push(...(d.testSpecificationIds ?? []));
|
|
625
|
+
/* 시각이 열쇠에 들어가므로 이 표는 트윈이 굴러가는 동안 계속 자란다 — 크기를 묶는다. */
|
|
626
|
+
if (cache.size >= REQUIRED_TESTS_MAX)
|
|
627
|
+
cache.clear();
|
|
628
|
+
cache.set(key, required);
|
|
547
629
|
return required;
|
|
548
630
|
}
|
|
631
|
+
const REQUIRED_TESTS_MAX = 512;
|
|
632
|
+
const requiredTestsCache = new WeakMap();
|
|
549
633
|
/**
|
|
550
634
|
* 자원의 **가용 능력을 판정한다** — 하나의 규칙, 하나의 자리.
|
|
551
635
|
*
|
package/dist/flow-engine.d.ts
CHANGED
|
@@ -475,7 +475,21 @@ export declare abstract class FlowEngine implements TwinKernel {
|
|
|
475
475
|
* 이미 시계가 흐른 뒤에 옮기면 그전에 낸 사실들과 시간축이 어긋나므로 **기동 직후에만** 부른다.
|
|
476
476
|
*/
|
|
477
477
|
setClockOrigin(originMs: number): void;
|
|
478
|
+
/**
|
|
479
|
+
* 이 커널의 **지금**(ISO 문자열).
|
|
480
|
+
*
|
|
481
|
+
* ── 같은 시각을 몇천 번 다시 문자열로 만들지 않는다 (2026-08-17 프로파일) ──
|
|
482
|
+
* 능력 판정이 이 값을 자원마다·작업마다 부른다. 그래서 한 틱 안에서 이 함수가 수백 번 불리는데,
|
|
483
|
+
* 그때마다 `new Date(...).toISOString()` 이 새 문자열을 지었다 — 프로파일에서 **틱 시간의 40%** 가
|
|
484
|
+
* 여기였다.
|
|
485
|
+
*
|
|
486
|
+
* 밀리초가 같으면 문자열도 같다. 그래서 밀리초를 열쇠로 기억한다 — 근사가 아니라 **정확히 같은 값**을
|
|
487
|
+
* 내므로 판정은 하나도 달라지지 않는다. 덤으로 같은 문자열 인스턴스가 돌아오므로 그것을 다시 파싱하는
|
|
488
|
+
* 쪽(`effectivityAt`)의 캐시도 한 번에 맞는다.
|
|
489
|
+
*/
|
|
478
490
|
protected now(): string;
|
|
491
|
+
private nowIsoMs;
|
|
492
|
+
private nowIso;
|
|
479
493
|
/**
|
|
480
494
|
* 자극이 선언한 **약속**을 오더 필드로 — 표준 `OperationsRequest.Priority`·`StartTime`·`EndTime`.
|
|
481
495
|
*
|
package/dist/flow-engine.js
CHANGED
|
@@ -1062,7 +1062,28 @@ export class FlowEngine {
|
|
|
1062
1062
|
throw new Error('clock origin must be a finite epoch millisecond value');
|
|
1063
1063
|
this.originMs = originMs;
|
|
1064
1064
|
}
|
|
1065
|
-
|
|
1065
|
+
/**
|
|
1066
|
+
* 이 커널의 **지금**(ISO 문자열).
|
|
1067
|
+
*
|
|
1068
|
+
* ── 같은 시각을 몇천 번 다시 문자열로 만들지 않는다 (2026-08-17 프로파일) ──
|
|
1069
|
+
* 능력 판정이 이 값을 자원마다·작업마다 부른다. 그래서 한 틱 안에서 이 함수가 수백 번 불리는데,
|
|
1070
|
+
* 그때마다 `new Date(...).toISOString()` 이 새 문자열을 지었다 — 프로파일에서 **틱 시간의 40%** 가
|
|
1071
|
+
* 여기였다.
|
|
1072
|
+
*
|
|
1073
|
+
* 밀리초가 같으면 문자열도 같다. 그래서 밀리초를 열쇠로 기억한다 — 근사가 아니라 **정확히 같은 값**을
|
|
1074
|
+
* 내므로 판정은 하나도 달라지지 않는다. 덤으로 같은 문자열 인스턴스가 돌아오므로 그것을 다시 파싱하는
|
|
1075
|
+
* 쪽(`effectivityAt`)의 캐시도 한 번에 맞는다.
|
|
1076
|
+
*/
|
|
1077
|
+
now() {
|
|
1078
|
+
const ms = this.nowMs();
|
|
1079
|
+
if (ms !== this.nowIsoMs) {
|
|
1080
|
+
this.nowIsoMs = ms;
|
|
1081
|
+
this.nowIso = new Date(ms).toISOString();
|
|
1082
|
+
}
|
|
1083
|
+
return this.nowIso;
|
|
1084
|
+
}
|
|
1085
|
+
nowIsoMs = Number.NaN;
|
|
1086
|
+
nowIso = '';
|
|
1066
1087
|
/**
|
|
1067
1088
|
* 자극이 선언한 **약속**을 오더 필드로 — 표준 `OperationsRequest.Priority`·`StartTime`·`EndTime`.
|
|
1068
1089
|
*
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -261,16 +261,36 @@ function meetsTests(required, results, at) {
|
|
|
261
261
|
}
|
|
262
262
|
function effectivityAt(p, at) {
|
|
263
263
|
if (!p || !at) return void 0;
|
|
264
|
-
const atMs =
|
|
264
|
+
const atMs = parsedMs(at);
|
|
265
265
|
if (!Number.isFinite(atMs)) return void 0;
|
|
266
|
-
const from = p.effectiveStart ?
|
|
266
|
+
const from = p.effectiveStart ? parsedMs(p.effectiveStart) : NaN;
|
|
267
267
|
if (Number.isFinite(from) && atMs < from) return "not-yet";
|
|
268
|
-
const to = p.effectiveEnd ?
|
|
268
|
+
const to = p.effectiveEnd ? parsedMs(p.effectiveEnd) : NaN;
|
|
269
269
|
if (Number.isFinite(to) && atMs > to) return "expired";
|
|
270
270
|
return void 0;
|
|
271
271
|
}
|
|
272
|
+
var PARSED_MAX = 64;
|
|
273
|
+
var parsedCache = /* @__PURE__ */ new Map();
|
|
274
|
+
function parsedMs(at) {
|
|
275
|
+
const hit = parsedCache.get(at);
|
|
276
|
+
if (hit !== void 0) return hit;
|
|
277
|
+
const ms2 = Date.parse(at);
|
|
278
|
+
if (parsedCache.size >= PARSED_MAX) parsedCache.clear();
|
|
279
|
+
parsedCache.set(at, ms2);
|
|
280
|
+
return ms2;
|
|
281
|
+
}
|
|
282
|
+
var classIndexCache = /* @__PURE__ */ new WeakMap();
|
|
283
|
+
function classIndex(defs) {
|
|
284
|
+
if (!defs) return EMPTY_CLASS_INDEX;
|
|
285
|
+
const hit = classIndexCache.get(defs);
|
|
286
|
+
if (hit) return hit;
|
|
287
|
+
const built = new Map(defs.map((d) => [d.id, d]));
|
|
288
|
+
classIndexCache.set(defs, built);
|
|
289
|
+
return built;
|
|
290
|
+
}
|
|
291
|
+
var EMPTY_CLASS_INDEX = /* @__PURE__ */ new Map();
|
|
272
292
|
function classClosure(directIds, defs, at) {
|
|
273
|
-
const byId =
|
|
293
|
+
const byId = classIndex(defs);
|
|
274
294
|
const inWindow = (d) => !d || effectivityAt(d, at) === void 0;
|
|
275
295
|
const out = /* @__PURE__ */ new Set();
|
|
276
296
|
const stack = [...directIds ?? []];
|
|
@@ -409,8 +429,10 @@ function activeShiftAt(entries, atMs, utcOffsetMinutes) {
|
|
|
409
429
|
return void 0;
|
|
410
430
|
}
|
|
411
431
|
function minuteOfDayAt(ms2, utcOffsetMinutes) {
|
|
412
|
-
const
|
|
413
|
-
|
|
432
|
+
const shifted = ms2 + (utcOffsetMinutes ?? 0) * 6e4;
|
|
433
|
+
const DAY = 864e5;
|
|
434
|
+
const inDay = (shifted % DAY + DAY) % DAY;
|
|
435
|
+
return Math.floor(inDay / 6e4);
|
|
414
436
|
}
|
|
415
437
|
function offCalendarReasonAt(r, ms2, utcOffsetMinutes) {
|
|
416
438
|
if (!offCalendarAt(r, ms2, utcOffsetMinutes)) return void 0;
|
|
@@ -435,11 +457,20 @@ function offCalendarAt(r, ms2, utcOffsetMinutes) {
|
|
|
435
457
|
}
|
|
436
458
|
function requiredTestsFor(directIds, defs, at) {
|
|
437
459
|
if (!defs?.length) return [];
|
|
460
|
+
const cache = requiredTestsCache.get(defs) ?? /* @__PURE__ */ new Map();
|
|
461
|
+
if (!requiredTestsCache.has(defs)) requiredTestsCache.set(defs, cache);
|
|
462
|
+
const key = `${at ?? ""}\0${(directIds ?? []).join("")}`;
|
|
463
|
+
const hit = cache.get(key);
|
|
464
|
+
if (hit) return hit;
|
|
438
465
|
const closure = classClosure(directIds, defs, at);
|
|
439
466
|
const required = [];
|
|
440
467
|
for (const d of defs) if (closure.has(d.id)) required.push(...d.testSpecificationIds ?? []);
|
|
468
|
+
if (cache.size >= REQUIRED_TESTS_MAX) cache.clear();
|
|
469
|
+
cache.set(key, required);
|
|
441
470
|
return required;
|
|
442
471
|
}
|
|
472
|
+
var REQUIRED_TESTS_MAX = 512;
|
|
473
|
+
var requiredTestsCache = /* @__PURE__ */ new WeakMap();
|
|
443
474
|
function capabilityOf(r, ctx) {
|
|
444
475
|
const at = ctx?.at;
|
|
445
476
|
const eff = effectivityAt(r, at);
|
|
@@ -3621,9 +3652,28 @@ var FlowEngine = class {
|
|
|
3621
3652
|
if (!Number.isFinite(originMs)) throw new Error("clock origin must be a finite epoch millisecond value");
|
|
3622
3653
|
this.originMs = originMs;
|
|
3623
3654
|
}
|
|
3655
|
+
/**
|
|
3656
|
+
* 이 커널의 **지금**(ISO 문자열).
|
|
3657
|
+
*
|
|
3658
|
+
* ── 같은 시각을 몇천 번 다시 문자열로 만들지 않는다 (2026-08-17 프로파일) ──
|
|
3659
|
+
* 능력 판정이 이 값을 자원마다·작업마다 부른다. 그래서 한 틱 안에서 이 함수가 수백 번 불리는데,
|
|
3660
|
+
* 그때마다 `new Date(...).toISOString()` 이 새 문자열을 지었다 — 프로파일에서 **틱 시간의 40%** 가
|
|
3661
|
+
* 여기였다.
|
|
3662
|
+
*
|
|
3663
|
+
* 밀리초가 같으면 문자열도 같다. 그래서 밀리초를 열쇠로 기억한다 — 근사가 아니라 **정확히 같은 값**을
|
|
3664
|
+
* 내므로 판정은 하나도 달라지지 않는다. 덤으로 같은 문자열 인스턴스가 돌아오므로 그것을 다시 파싱하는
|
|
3665
|
+
* 쪽(`effectivityAt`)의 캐시도 한 번에 맞는다.
|
|
3666
|
+
*/
|
|
3624
3667
|
now() {
|
|
3625
|
-
|
|
3668
|
+
const ms2 = this.nowMs();
|
|
3669
|
+
if (ms2 !== this.nowIsoMs) {
|
|
3670
|
+
this.nowIsoMs = ms2;
|
|
3671
|
+
this.nowIso = new Date(ms2).toISOString();
|
|
3672
|
+
}
|
|
3673
|
+
return this.nowIso;
|
|
3626
3674
|
}
|
|
3675
|
+
nowIsoMs = Number.NaN;
|
|
3676
|
+
nowIso = "";
|
|
3627
3677
|
/**
|
|
3628
3678
|
* 자극이 선언한 **약속**을 오더 필드로 — 표준 `OperationsRequest.Priority`·`StartTime`·`EndTime`.
|
|
3629
3679
|
*
|
package/package.json
CHANGED