@operato/twin-kernel 0.7.10 → 0.7.12
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 +28 -8
- package/dist/contract.js +90 -6
- package/dist/flow-engine.d.ts +14 -0
- package/dist/flow-engine.js +37 -4
- package/dist/observed-reducer.js +4 -0
- package/dist/yms-kernel.d.ts +2 -0
- package/dist/yms-kernel.js +21 -1
- package/dist-cjs/index.cjs +88 -11
- 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` =
|
|
@@ -1133,6 +1125,20 @@ export interface OrderState {
|
|
|
1133
1125
|
* 이것도 상태에 없어서 되살아난 오더는 빈 문자열을 갖고, 이후 모든 이벤트의 거래번호가 빈다.
|
|
1134
1126
|
*/
|
|
1135
1127
|
bizTransaction?: string;
|
|
1128
|
+
/**
|
|
1129
|
+
* 도메인이 **약속해 둔 자리와 시각창** — 어디로/언제 들이기로 했는가.
|
|
1130
|
+
*
|
|
1131
|
+
* ── 왜 사실에 실어야 하나 (2026-08-17) ─────────────────────────────────────
|
|
1132
|
+
* 야드 트윈이 재기동 뒤 **첫 틱에서 죽었다.** 어포인트먼트를 받을 때 도크 도어를 정해 두는데
|
|
1133
|
+
* 그것이 어느 사실에도 실리지 않아, 웜스타트가 오더는 되살리고 배정은 잃었다. 그리고 다음 틱에
|
|
1134
|
+
* 「그 도어의 점유」를 읽다 예외가 났다(`Cannot read properties of undefined`). 안전망이 그 트윈만
|
|
1135
|
+
* 세워 다른 트윈은 살았지만, 야드 트윈 셋이 부팅마다 죽는 상태였다.
|
|
1136
|
+
*
|
|
1137
|
+
* `allocated` 가 두 자리에만 있어 계보를 잃었던 것과 **같은 부류**다: 상태가 필요로 하는 것을
|
|
1138
|
+
* 사실이 갖고 있지 않으면 재기동을 넘지 못한다.
|
|
1139
|
+
*/
|
|
1140
|
+
dockDoor?: string;
|
|
1141
|
+
windowStartMs?: number;
|
|
1136
1142
|
/** 우선순위 — 표준 `OperationsRequest.Priority`. 작은 값이 급하다. 오더 할당 순서를 정한다. */
|
|
1137
1143
|
priority?: number;
|
|
1138
1144
|
/** 예정 착수 — 표준 `OperationsRequest.StartTime`. */
|
|
@@ -1678,6 +1684,20 @@ export interface OrderStatusDelta {
|
|
|
1678
1684
|
*/
|
|
1679
1685
|
allocated?: string[];
|
|
1680
1686
|
bizTransaction?: string;
|
|
1687
|
+
/**
|
|
1688
|
+
* 도메인이 **약속해 둔 자리와 시각창** — 어디로/언제 들이기로 했는가.
|
|
1689
|
+
*
|
|
1690
|
+
* ── 왜 사실에 실어야 하나 (2026-08-17) ─────────────────────────────────────
|
|
1691
|
+
* 야드 트윈이 재기동 뒤 **첫 틱에서 죽었다.** 어포인트먼트를 받을 때 도크 도어를 정해 두는데
|
|
1692
|
+
* 그것이 어느 사실에도 실리지 않아, 웜스타트가 오더는 되살리고 배정은 잃었다. 그리고 다음 틱에
|
|
1693
|
+
* 「그 도어의 점유」를 읽다 예외가 났다(`Cannot read properties of undefined`). 안전망이 그 트윈만
|
|
1694
|
+
* 세워 다른 트윈은 살았지만, 야드 트윈 셋이 부팅마다 죽는 상태였다.
|
|
1695
|
+
*
|
|
1696
|
+
* `allocated` 가 두 자리에만 있어 계보를 잃었던 것과 **같은 부류**다: 상태가 필요로 하는 것을
|
|
1697
|
+
* 사실이 갖고 있지 않으면 재기동을 넘지 못한다.
|
|
1698
|
+
*/
|
|
1699
|
+
dockDoor?: string;
|
|
1700
|
+
windowStartMs?: number;
|
|
1681
1701
|
}
|
|
1682
1702
|
export declare const CMD: {
|
|
1683
1703
|
readonly orderHold: "order.hold";
|
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
|
@@ -589,7 +589,9 @@ export class FlowEngine {
|
|
|
589
589
|
사라진다(같은 사실이 델타·스냅샷·이 변환 **세 길**을 지난다). */
|
|
590
590
|
...(o.allocated?.length ? { allocated: o.allocated } : {}),
|
|
591
591
|
...(o.gtin ? { gtin: o.gtin } : {}),
|
|
592
|
-
...(o.bizTransaction ? { bizTransaction: o.bizTransaction } : {})
|
|
592
|
+
...(o.bizTransaction ? { bizTransaction: o.bizTransaction } : {}),
|
|
593
|
+
...(o.dockDoor ? { dockDoor: o.dockDoor } : {}),
|
|
594
|
+
...(o.windowStartMs !== undefined ? { windowStartMs: o.windowStartMs } : {})
|
|
593
595
|
}));
|
|
594
596
|
for (const o of observedOrders) {
|
|
595
597
|
const lines = (o.lines ?? []).map(l => ({ gtin: l.gtin, requested: l.requested - (l.fulfilled ?? 0) })).filter(l => l.requested > 0);
|
|
@@ -614,7 +616,11 @@ export class FlowEngine {
|
|
|
614
616
|
...(o.held ? { held: true } : {}),
|
|
615
617
|
...(o.endTime ? { endTime: o.endTime } : {}),
|
|
616
618
|
...(o.startTime ? { startTime: o.startTime } : {}),
|
|
617
|
-
...(o.priority !== undefined ? { priority: o.priority } : {})
|
|
619
|
+
...(o.priority !== undefined ? { priority: o.priority } : {}),
|
|
620
|
+
/* 약속해 둔 자리·시각창을 이어받는다 — 없으면 도메인이 「정해 뒀다」고 믿는 것을 잃는다.
|
|
621
|
+
사실이 말해 주지 않으면(옛 저널) 비는 것이 사실이다 — 지어내지 않는다. */
|
|
622
|
+
...(o.dockDoor ? { dockDoor: o.dockDoor } : {}),
|
|
623
|
+
...(o.windowStartMs !== undefined ? { windowStartMs: o.windowStartMs } : {})
|
|
618
624
|
});
|
|
619
625
|
}
|
|
620
626
|
/* 작업이 딛고 설 오더를 먼저 세운다 — 순서가 뒤바뀌면 아래 확인이 언제나 "없다" 로 답한다. */
|
|
@@ -955,6 +961,9 @@ export class FlowEngine {
|
|
|
955
961
|
/* 진행 중 확보분과 거래번호 — **스냅샷에도** 실어야 한다. 델타에만 실으면 스냅샷으로
|
|
956
962
|
재기동하는 경로(체크포인트)에서 그대로 잃는다(같은 사실을 두 길로 나르는 값이다). */
|
|
957
963
|
...(o.allocated?.length ? { allocated: o.allocated.slice() } : {}),
|
|
964
|
+
/* 스냅샷도 함께 나른다 — 델타·스냅샷·복원 **세 길** 중 하나만 빠져도 재기동이 잃는다. */
|
|
965
|
+
...(o.dockDoor ? { dockDoor: o.dockDoor } : {}),
|
|
966
|
+
...(o.windowStartMs !== undefined ? { windowStartMs: o.windowStartMs } : {}),
|
|
958
967
|
...(o.bizTransaction ? { bizTransaction: o.bizTransaction } : {}),
|
|
959
968
|
held: o.held
|
|
960
969
|
})),
|
|
@@ -1062,7 +1071,28 @@ export class FlowEngine {
|
|
|
1062
1071
|
throw new Error('clock origin must be a finite epoch millisecond value');
|
|
1063
1072
|
this.originMs = originMs;
|
|
1064
1073
|
}
|
|
1065
|
-
|
|
1074
|
+
/**
|
|
1075
|
+
* 이 커널의 **지금**(ISO 문자열).
|
|
1076
|
+
*
|
|
1077
|
+
* ── 같은 시각을 몇천 번 다시 문자열로 만들지 않는다 (2026-08-17 프로파일) ──
|
|
1078
|
+
* 능력 판정이 이 값을 자원마다·작업마다 부른다. 그래서 한 틱 안에서 이 함수가 수백 번 불리는데,
|
|
1079
|
+
* 그때마다 `new Date(...).toISOString()` 이 새 문자열을 지었다 — 프로파일에서 **틱 시간의 40%** 가
|
|
1080
|
+
* 여기였다.
|
|
1081
|
+
*
|
|
1082
|
+
* 밀리초가 같으면 문자열도 같다. 그래서 밀리초를 열쇠로 기억한다 — 근사가 아니라 **정확히 같은 값**을
|
|
1083
|
+
* 내므로 판정은 하나도 달라지지 않는다. 덤으로 같은 문자열 인스턴스가 돌아오므로 그것을 다시 파싱하는
|
|
1084
|
+
* 쪽(`effectivityAt`)의 캐시도 한 번에 맞는다.
|
|
1085
|
+
*/
|
|
1086
|
+
now() {
|
|
1087
|
+
const ms = this.nowMs();
|
|
1088
|
+
if (ms !== this.nowIsoMs) {
|
|
1089
|
+
this.nowIsoMs = ms;
|
|
1090
|
+
this.nowIso = new Date(ms).toISOString();
|
|
1091
|
+
}
|
|
1092
|
+
return this.nowIso;
|
|
1093
|
+
}
|
|
1094
|
+
nowIsoMs = Number.NaN;
|
|
1095
|
+
nowIso = '';
|
|
1066
1096
|
/**
|
|
1067
1097
|
* 자극이 선언한 **약속**을 오더 필드로 — 표준 `OperationsRequest.Priority`·`StartTime`·`EndTime`.
|
|
1068
1098
|
*
|
|
@@ -1639,7 +1669,10 @@ export class FlowEngine {
|
|
|
1639
1669
|
계보가 입력 없이 나간다(무엇이 무엇으로 바뀌었나의 절반이 사라진다). */
|
|
1640
1670
|
...(o.gtin ? { gtin: o.gtin } : {}),
|
|
1641
1671
|
...(o.allocated?.length ? { allocated: o.allocated.slice() } : {}),
|
|
1642
|
-
...(o.bizTransaction ? { bizTransaction: o.bizTransaction } : {})
|
|
1672
|
+
...(o.bizTransaction ? { bizTransaction: o.bizTransaction } : {}),
|
|
1673
|
+
/* 약속해 둔 자리·시각창 — 이것이 빠져서 야드 트윈이 재기동마다 첫 틱에 죽었다(§dockDoor). */
|
|
1674
|
+
...(o.dockDoor ? { dockDoor: o.dockDoor } : {}),
|
|
1675
|
+
...(o.windowStartMs !== undefined ? { windowStartMs: o.windowStartMs } : {})
|
|
1643
1676
|
});
|
|
1644
1677
|
}
|
|
1645
1678
|
// ── 내부 mechanics ─────────────────────────────────────────────────────────
|
package/dist/observed-reducer.js
CHANGED
|
@@ -357,6 +357,10 @@ export class ObservedReducer {
|
|
|
357
357
|
...(d.gtin ? { gtin: d.gtin } : {}),
|
|
358
358
|
...(d.allocated?.length ? { allocated: d.allocated.slice() } : {}),
|
|
359
359
|
...(d.bizTransaction ? { bizTransaction: d.bizTransaction } : {}),
|
|
360
|
+
/* 약속해 둔 자리·시각창도 채운다 — 시뮬만 알면 미러 위 예측이 「어디로 들일지」를 모른다.
|
|
361
|
+
(이 필드가 어느 길에서 빠지면 무슨 일이 나는지는 §dockDoor 에 적혀 있다.) */
|
|
362
|
+
...(d.dockDoor ? { dockDoor: d.dockDoor } : {}),
|
|
363
|
+
...(d.windowStartMs !== undefined ? { windowStartMs: d.windowStartMs } : {}),
|
|
360
364
|
held: d.held
|
|
361
365
|
});
|
|
362
366
|
break;
|
package/dist/yms-kernel.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ export declare class YmsKernel extends FlowEngine {
|
|
|
8
8
|
private cargoSeq;
|
|
9
9
|
private trailerCargo;
|
|
10
10
|
private apptMode;
|
|
11
|
+
/** 도어를 못 찾아 대기시킨 오더 — 같은 말을 매 틱 반복하지 않기 위한 표시(§allocate). */
|
|
12
|
+
private doorWarned;
|
|
11
13
|
/** 야드 운영 정책(상하차 방식). 기본 drop(야드 버퍼). live 는 게이트/도크 직행. */
|
|
12
14
|
mode: YardMode;
|
|
13
15
|
constructor(tenantId: string, policy?: AllocationPolicy, mode?: YardMode);
|
package/dist/yms-kernel.js
CHANGED
|
@@ -39,6 +39,8 @@ export class YmsKernel extends FlowEngine {
|
|
|
39
39
|
cargoSeq = 0;
|
|
40
40
|
trailerCargo = new Map(); // 트레일러 epc → 화물 SSCC(하차 전 opaque / 상차 예약분)
|
|
41
41
|
apptMode = new Map(); // orderId → 상하차 모드(생성 시 캡처)
|
|
42
|
+
/** 도어를 못 찾아 대기시킨 오더 — 같은 말을 매 틱 반복하지 않기 위한 표시(§allocate). */
|
|
43
|
+
doorWarned = new Set();
|
|
42
44
|
/** 야드 운영 정책(상하차 방식). 기본 drop(야드 버퍼). live 는 게이트/도크 직행. */
|
|
43
45
|
mode;
|
|
44
46
|
constructor(tenantId, policy = firstFitPolicy, mode = 'drop') {
|
|
@@ -102,7 +104,25 @@ export class YmsKernel extends FlowEngine {
|
|
|
102
104
|
return;
|
|
103
105
|
if (this.clockMs < (o.windowStartMs ?? 0))
|
|
104
106
|
return; // 창 도래 전
|
|
105
|
-
|
|
107
|
+
/*
|
|
108
|
+
* ── 배정된 도어가 없으면 **기다린다** (2026-08-17) ────────────────────────
|
|
109
|
+
*
|
|
110
|
+
* 예전에는 `this.locations.get(o.dockDoor!)!` 였다. 그래서 배정을 모르는 오더가 들어오면 바로
|
|
111
|
+
* 아래에서 `undefined.occupancy` 를 읽고 **틱 전체가 예외로 죽었다** — 야드 트윈 셋이 재기동마다
|
|
112
|
+
* 그렇게 멈췄다(안전망이 세운 것이지 스스로 멈춘 것이 아니다).
|
|
113
|
+
*
|
|
114
|
+
* 이제 그 오더만 대기로 남기고 **왜인지 한 번 말한다.** 매 틱 말하면 로그가 그것만 남으므로 한 번만
|
|
115
|
+
* 말하고, 조용히 넘기지도 않는다 — 조용하면 「왜 이 오더는 영원히 안 움직이나」를 아무도 답할 수 없다.
|
|
116
|
+
*/
|
|
117
|
+
const door = o.dockDoor ? this.locations.get(o.dockDoor) : undefined;
|
|
118
|
+
if (!door) {
|
|
119
|
+
if (!this.doorWarned.has(o.id)) {
|
|
120
|
+
this.doorWarned.add(o.id);
|
|
121
|
+
console.warn(`[yms-kernel] order "${o.id}" waits: ${o.dockDoor ? `its dock door "${o.dockDoor}" is not in this model` : 'no dock door was assigned to it'}` +
|
|
122
|
+
' — the appointment cannot be scheduled until the declaration says where.');
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
106
126
|
const inflight = [...this.tasks.values()].some(t => t.status !== 'completed' && t.toNode === door.id);
|
|
107
127
|
if (door.occupancy > 0 || inflight)
|
|
108
128
|
return; // 도어 사용중/예약됨 → 대기(직렬화)
|
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);
|
|
@@ -1323,6 +1354,10 @@ var ObservedReducer = class {
|
|
|
1323
1354
|
...d.gtin ? { gtin: d.gtin } : {},
|
|
1324
1355
|
...d.allocated?.length ? { allocated: d.allocated.slice() } : {},
|
|
1325
1356
|
...d.bizTransaction ? { bizTransaction: d.bizTransaction } : {},
|
|
1357
|
+
/* 약속해 둔 자리·시각창도 채운다 — 시뮬만 알면 미러 위 예측이 「어디로 들일지」를 모른다.
|
|
1358
|
+
(이 필드가 어느 길에서 빠지면 무슨 일이 나는지는 §dockDoor 에 적혀 있다.) */
|
|
1359
|
+
...d.dockDoor ? { dockDoor: d.dockDoor } : {},
|
|
1360
|
+
...d.windowStartMs !== void 0 ? { windowStartMs: d.windowStartMs } : {},
|
|
1326
1361
|
held: d.held
|
|
1327
1362
|
});
|
|
1328
1363
|
break;
|
|
@@ -3188,7 +3223,9 @@ var FlowEngine = class {
|
|
|
3188
3223
|
사라진다(같은 사실이 델타·스냅샷·이 변환 **세 길**을 지난다). */
|
|
3189
3224
|
...o.allocated?.length ? { allocated: o.allocated } : {},
|
|
3190
3225
|
...o.gtin ? { gtin: o.gtin } : {},
|
|
3191
|
-
...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {}
|
|
3226
|
+
...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {},
|
|
3227
|
+
...o.dockDoor ? { dockDoor: o.dockDoor } : {},
|
|
3228
|
+
...o.windowStartMs !== void 0 ? { windowStartMs: o.windowStartMs } : {}
|
|
3192
3229
|
}));
|
|
3193
3230
|
for (const o of observedOrders) {
|
|
3194
3231
|
const lines = (o.lines ?? []).map((l) => ({ gtin: l.gtin, requested: l.requested - (l.fulfilled ?? 0) })).filter((l) => l.requested > 0);
|
|
@@ -3215,7 +3252,11 @@ var FlowEngine = class {
|
|
|
3215
3252
|
...o.held ? { held: true } : {},
|
|
3216
3253
|
...o.endTime ? { endTime: o.endTime } : {},
|
|
3217
3254
|
...o.startTime ? { startTime: o.startTime } : {},
|
|
3218
|
-
...o.priority !== void 0 ? { priority: o.priority } : {}
|
|
3255
|
+
...o.priority !== void 0 ? { priority: o.priority } : {},
|
|
3256
|
+
/* 약속해 둔 자리·시각창을 이어받는다 — 없으면 도메인이 「정해 뒀다」고 믿는 것을 잃는다.
|
|
3257
|
+
사실이 말해 주지 않으면(옛 저널) 비는 것이 사실이다 — 지어내지 않는다. */
|
|
3258
|
+
...o.dockDoor ? { dockDoor: o.dockDoor } : {},
|
|
3259
|
+
...o.windowStartMs !== void 0 ? { windowStartMs: o.windowStartMs } : {}
|
|
3219
3260
|
});
|
|
3220
3261
|
}
|
|
3221
3262
|
const seededOrderIds = new Set(this.orders.keys());
|
|
@@ -3531,6 +3572,9 @@ var FlowEngine = class {
|
|
|
3531
3572
|
/* 진행 중 확보분과 거래번호 — **스냅샷에도** 실어야 한다. 델타에만 실으면 스냅샷으로
|
|
3532
3573
|
재기동하는 경로(체크포인트)에서 그대로 잃는다(같은 사실을 두 길로 나르는 값이다). */
|
|
3533
3574
|
...o.allocated?.length ? { allocated: o.allocated.slice() } : {},
|
|
3575
|
+
/* 스냅샷도 함께 나른다 — 델타·스냅샷·복원 **세 길** 중 하나만 빠져도 재기동이 잃는다. */
|
|
3576
|
+
...o.dockDoor ? { dockDoor: o.dockDoor } : {},
|
|
3577
|
+
...o.windowStartMs !== void 0 ? { windowStartMs: o.windowStartMs } : {},
|
|
3534
3578
|
...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {},
|
|
3535
3579
|
held: o.held
|
|
3536
3580
|
})),
|
|
@@ -3621,9 +3665,28 @@ var FlowEngine = class {
|
|
|
3621
3665
|
if (!Number.isFinite(originMs)) throw new Error("clock origin must be a finite epoch millisecond value");
|
|
3622
3666
|
this.originMs = originMs;
|
|
3623
3667
|
}
|
|
3668
|
+
/**
|
|
3669
|
+
* 이 커널의 **지금**(ISO 문자열).
|
|
3670
|
+
*
|
|
3671
|
+
* ── 같은 시각을 몇천 번 다시 문자열로 만들지 않는다 (2026-08-17 프로파일) ──
|
|
3672
|
+
* 능력 판정이 이 값을 자원마다·작업마다 부른다. 그래서 한 틱 안에서 이 함수가 수백 번 불리는데,
|
|
3673
|
+
* 그때마다 `new Date(...).toISOString()` 이 새 문자열을 지었다 — 프로파일에서 **틱 시간의 40%** 가
|
|
3674
|
+
* 여기였다.
|
|
3675
|
+
*
|
|
3676
|
+
* 밀리초가 같으면 문자열도 같다. 그래서 밀리초를 열쇠로 기억한다 — 근사가 아니라 **정확히 같은 값**을
|
|
3677
|
+
* 내므로 판정은 하나도 달라지지 않는다. 덤으로 같은 문자열 인스턴스가 돌아오므로 그것을 다시 파싱하는
|
|
3678
|
+
* 쪽(`effectivityAt`)의 캐시도 한 번에 맞는다.
|
|
3679
|
+
*/
|
|
3624
3680
|
now() {
|
|
3625
|
-
|
|
3681
|
+
const ms2 = this.nowMs();
|
|
3682
|
+
if (ms2 !== this.nowIsoMs) {
|
|
3683
|
+
this.nowIsoMs = ms2;
|
|
3684
|
+
this.nowIso = new Date(ms2).toISOString();
|
|
3685
|
+
}
|
|
3686
|
+
return this.nowIso;
|
|
3626
3687
|
}
|
|
3688
|
+
nowIsoMs = Number.NaN;
|
|
3689
|
+
nowIso = "";
|
|
3627
3690
|
/**
|
|
3628
3691
|
* 자극이 선언한 **약속**을 오더 필드로 — 표준 `OperationsRequest.Priority`·`StartTime`·`EndTime`.
|
|
3629
3692
|
*
|
|
@@ -4158,7 +4221,10 @@ var FlowEngine = class {
|
|
|
4158
4221
|
계보가 입력 없이 나간다(무엇이 무엇으로 바뀌었나의 절반이 사라진다). */
|
|
4159
4222
|
...o.gtin ? { gtin: o.gtin } : {},
|
|
4160
4223
|
...o.allocated?.length ? { allocated: o.allocated.slice() } : {},
|
|
4161
|
-
...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {}
|
|
4224
|
+
...o.bizTransaction ? { bizTransaction: o.bizTransaction } : {},
|
|
4225
|
+
/* 약속해 둔 자리·시각창 — 이것이 빠져서 야드 트윈이 재기동마다 첫 틱에 죽었다(§dockDoor). */
|
|
4226
|
+
...o.dockDoor ? { dockDoor: o.dockDoor } : {},
|
|
4227
|
+
...o.windowStartMs !== void 0 ? { windowStartMs: o.windowStartMs } : {}
|
|
4162
4228
|
});
|
|
4163
4229
|
}
|
|
4164
4230
|
// ── 내부 mechanics ─────────────────────────────────────────────────────────
|
|
@@ -5250,6 +5316,8 @@ var YmsKernel = class extends FlowEngine {
|
|
|
5250
5316
|
// 트레일러 epc → 화물 SSCC(하차 전 opaque / 상차 예약분)
|
|
5251
5317
|
apptMode = /* @__PURE__ */ new Map();
|
|
5252
5318
|
// orderId → 상하차 모드(생성 시 캡처)
|
|
5319
|
+
/** 도어를 못 찾아 대기시킨 오더 — 같은 말을 매 틱 반복하지 않기 위한 표시(§allocate). */
|
|
5320
|
+
doorWarned = /* @__PURE__ */ new Set();
|
|
5253
5321
|
/** 야드 운영 정책(상하차 방식). 기본 drop(야드 버퍼). live 는 게이트/도크 직행. */
|
|
5254
5322
|
mode;
|
|
5255
5323
|
constructor(tenantId, policy = firstFitPolicy, mode = "drop") {
|
|
@@ -5321,7 +5389,16 @@ var YmsKernel = class extends FlowEngine {
|
|
|
5321
5389
|
const trailer = this.items.get(o.allocated[0]);
|
|
5322
5390
|
if (!trailer) return;
|
|
5323
5391
|
if (this.clockMs < (o.windowStartMs ?? 0)) return;
|
|
5324
|
-
const door = this.locations.get(o.dockDoor);
|
|
5392
|
+
const door = o.dockDoor ? this.locations.get(o.dockDoor) : void 0;
|
|
5393
|
+
if (!door) {
|
|
5394
|
+
if (!this.doorWarned.has(o.id)) {
|
|
5395
|
+
this.doorWarned.add(o.id);
|
|
5396
|
+
console.warn(
|
|
5397
|
+
`[yms-kernel] order "${o.id}" waits: ${o.dockDoor ? `its dock door "${o.dockDoor}" is not in this model` : "no dock door was assigned to it"} \u2014 the appointment cannot be scheduled until the declaration says where.`
|
|
5398
|
+
);
|
|
5399
|
+
}
|
|
5400
|
+
return;
|
|
5401
|
+
}
|
|
5325
5402
|
const inflight = [...this.tasks.values()].some((t) => t.status !== "completed" && t.toNode === door.id);
|
|
5326
5403
|
if (door.occupancy > 0 || inflight) return;
|
|
5327
5404
|
const mode = this.apptMode.get(o.id) ?? "drop";
|
package/package.json
CHANGED