@operato/twin-kernel 0.7.37 → 0.7.38
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 +13 -0
- package/dist/flow-engine.js +5 -0
- package/dist/observed-reducer.d.ts +40 -1
- package/dist/observed-reducer.js +105 -7
- package/dist-cjs/index.cjs +81 -7
- package/package.json +1 -1
package/dist/contract.d.ts
CHANGED
|
@@ -1397,6 +1397,19 @@ export interface StateSnapshot {
|
|
|
1397
1397
|
tasks: TaskState[];
|
|
1398
1398
|
orders: OrderState[];
|
|
1399
1399
|
attentions?: Attention[];
|
|
1400
|
+
/**
|
|
1401
|
+
* **담을 줄 몰라 반영하지 못한 사실** — 종류별 수(관측 구동에서만 나온다).
|
|
1402
|
+
*
|
|
1403
|
+
* 관측 리듀서는 오래전부터 이것을 세어 왔다. 그런데 **스냅샷이 그 값을 떨어뜨렸다** — 그래서
|
|
1404
|
+
* 이미 그것을 읽도록 쓰여 있던 정합성 검사(`twin-unhandled-vocabulary`)는 미러에서 영원히
|
|
1405
|
+
* 조용했다(2026-08-19 실측). 세기만 하고 아무도 못 보는 값은 없는 것과 같다.
|
|
1406
|
+
*/
|
|
1407
|
+
unhandled?: {
|
|
1408
|
+
eventType: string;
|
|
1409
|
+
count: number;
|
|
1410
|
+
firstAtMs?: number;
|
|
1411
|
+
lastAtMs?: number;
|
|
1412
|
+
}[];
|
|
1400
1413
|
/**
|
|
1401
1414
|
* 확인(ack)해 둔 주목 신호 id — **상태에서 파생되지 않는 유일한 축.**
|
|
1402
1415
|
*
|
package/dist/flow-engine.js
CHANGED
|
@@ -1086,6 +1086,11 @@ export class FlowEngine {
|
|
|
1086
1086
|
/* 스냅샷이 **델타보다 가난하면 안 된다** — 예전에는 소요·남은 시간을 빼고 내보내서, 이 스냅샷으로
|
|
1087
1087
|
* 다른 커널을 주입하면(hydrateObserved) 진행 중이던 작업을 이어 굴릴 수 없었다(미러 스냅샷은
|
|
1088
1088
|
* 델타에서 왔으므로 갖고 있었다 — 같은 계약을 두 구동이 다르게 채우던 자리). */
|
|
1089
|
+
/* 관측이 담지 못한 사실을 그대로 내보낸다 — 리듀서가 세고 있어도 여기서 떨어뜨리면 아무도 못 본다. */
|
|
1090
|
+
...(() => {
|
|
1091
|
+
const u = this.observer?.snapshot?.().unhandled;
|
|
1092
|
+
return u?.length ? { unhandled: u } : {};
|
|
1093
|
+
})(),
|
|
1089
1094
|
tasks: [...this.tasks.values()].map(t => ({
|
|
1090
1095
|
id: t.id, kind: t.kind, status: t.status, itemRefs: [t.itemEpc],
|
|
1091
1096
|
fromNode: t.fromNode, toNode: t.toNode, resourceRef: t.resource ?? undefined, orderId: t.orderId,
|
|
@@ -106,6 +106,23 @@ export interface ReducerCheckpoint {
|
|
|
106
106
|
child: string;
|
|
107
107
|
parent: string;
|
|
108
108
|
}[];
|
|
109
|
+
/** 수량으로 담긴 내용 — 채워 넣은 것과, 팔레트를 아직 못 본 채 보류한 것. */
|
|
110
|
+
qtyAggregation?: {
|
|
111
|
+
parent: string;
|
|
112
|
+
quantities: {
|
|
113
|
+
epcClass?: string;
|
|
114
|
+
quantity?: number;
|
|
115
|
+
uom?: string;
|
|
116
|
+
}[];
|
|
117
|
+
}[];
|
|
118
|
+
pendingQuantities?: {
|
|
119
|
+
parent: string;
|
|
120
|
+
quantities: {
|
|
121
|
+
epcClass?: string;
|
|
122
|
+
quantity?: number;
|
|
123
|
+
uom?: string;
|
|
124
|
+
}[];
|
|
125
|
+
}[];
|
|
109
126
|
tasks: TaskState[];
|
|
110
127
|
equipment: EquipmentState[];
|
|
111
128
|
persons: PersonState[];
|
|
@@ -132,6 +149,15 @@ export declare class ObservedReducer {
|
|
|
132
149
|
private aggregation;
|
|
133
150
|
/** 아직 관측되지 않은 자식의 담김 — 물품을 지어내지 않고 보류했다가 등장할 때 붙인다. */
|
|
134
151
|
private pendingParent;
|
|
152
|
+
/**
|
|
153
|
+
* 팔레트가 **담고 있는 것**(클래스 + 수량) — 바코드 없이 수량만 실린 담김.
|
|
154
|
+
*
|
|
155
|
+
* · `qtyAggregation` — 우리가 그 팔레트 물품에 채워 넣은 내용(풀릴 때 되돌릴 근거).
|
|
156
|
+
* · `pendingQuantities` — 담김이 먼저 오고 그 팔레트를 아직 관측하지 못한 경우. 물품을 지어내지
|
|
157
|
+
* 않고 보류해 두었다가, 관측되는 순간 붙인다(`pendingParent` 와 같은 규율).
|
|
158
|
+
*/
|
|
159
|
+
private qtyAggregation;
|
|
160
|
+
private pendingQuantities;
|
|
135
161
|
private tasks;
|
|
136
162
|
private equipment;
|
|
137
163
|
private persons;
|
|
@@ -217,8 +243,21 @@ export declare class ObservedReducer {
|
|
|
217
243
|
private observedAtMs?;
|
|
218
244
|
/** 이벤트 1건 반영 — eventType 으로 EPCIS vs 운영 델타 분기. */
|
|
219
245
|
apply(e: CanonicalEnvelope): void;
|
|
220
|
-
/**
|
|
246
|
+
/**
|
|
247
|
+
* 반영하지 못한 사건을 종류별로 센다 — 처음·마지막 시각을 함께 남겨 「언제부터」에 답한다.
|
|
248
|
+
*
|
|
249
|
+
* `key` 를 주면 그 이름으로 센다: 종류 전체를 못 다룬 것과 **그 종류의 어떤 모양만** 못 다룬 것은
|
|
250
|
+
* 다른 사실이다(예: 담김은 반영하는데 품번이 섞인 팔레트만 못 담는다).
|
|
251
|
+
*/
|
|
221
252
|
private noteUnhandled;
|
|
253
|
+
/**
|
|
254
|
+
* 팔레트 물품에 **담고 있는 것**을 채운다 — 이미 아는 값을 덮지 않는다.
|
|
255
|
+
*
|
|
256
|
+
* 팔레트가 자기 품번·수량을 이미 들고 있으면(관측이 그렇게 말했으면) 담김 선언이 그것을 밀어내지
|
|
257
|
+
* 않는다. 비어 있던 자리만 채운다 — 「모른다」를 채우는 것이 이 작업의 목적이고, 아는 것을 바꾸는
|
|
258
|
+
* 것은 아니다.
|
|
259
|
+
*/
|
|
260
|
+
private withContainedQuantity;
|
|
222
261
|
private applyEpcis;
|
|
223
262
|
/**
|
|
224
263
|
* 물품 한 건 병합 — **아는 것을 잃지 않는다.** 새로 온 값이 우선, 없으면 기존 값 유지.
|
package/dist/observed-reducer.js
CHANGED
|
@@ -45,6 +45,15 @@ export class ObservedReducer {
|
|
|
45
45
|
aggregation = new Map();
|
|
46
46
|
/** 아직 관측되지 않은 자식의 담김 — 물품을 지어내지 않고 보류했다가 등장할 때 붙인다. */
|
|
47
47
|
pendingParent = new Map(); // 자식 EPC → 부모(물류단위)
|
|
48
|
+
/**
|
|
49
|
+
* 팔레트가 **담고 있는 것**(클래스 + 수량) — 바코드 없이 수량만 실린 담김.
|
|
50
|
+
*
|
|
51
|
+
* · `qtyAggregation` — 우리가 그 팔레트 물품에 채워 넣은 내용(풀릴 때 되돌릴 근거).
|
|
52
|
+
* · `pendingQuantities` — 담김이 먼저 오고 그 팔레트를 아직 관측하지 못한 경우. 물품을 지어내지
|
|
53
|
+
* 않고 보류해 두었다가, 관측되는 순간 붙인다(`pendingParent` 와 같은 규율).
|
|
54
|
+
*/
|
|
55
|
+
qtyAggregation = new Map();
|
|
56
|
+
pendingQuantities = new Map();
|
|
48
57
|
tasks = new Map();
|
|
49
58
|
equipment = new Map();
|
|
50
59
|
persons = new Map();
|
|
@@ -371,10 +380,16 @@ export class ObservedReducer {
|
|
|
371
380
|
this.noteUnhandled(e);
|
|
372
381
|
}
|
|
373
382
|
}
|
|
374
|
-
/**
|
|
375
|
-
|
|
383
|
+
/**
|
|
384
|
+
* 반영하지 못한 사건을 종류별로 센다 — 처음·마지막 시각을 함께 남겨 「언제부터」에 답한다.
|
|
385
|
+
*
|
|
386
|
+
* `key` 를 주면 그 이름으로 센다: 종류 전체를 못 다룬 것과 **그 종류의 어떤 모양만** 못 다룬 것은
|
|
387
|
+
* 다른 사실이다(예: 담김은 반영하는데 품번이 섞인 팔레트만 못 담는다).
|
|
388
|
+
*/
|
|
389
|
+
noteUnhandled(e, key) {
|
|
376
390
|
const at = Date.parse(String(e.eventTime ?? ''));
|
|
377
|
-
const
|
|
391
|
+
const name = key ?? e.eventType;
|
|
392
|
+
const cur = this.unhandled.get(name) ?? { count: 0 };
|
|
378
393
|
cur.count++;
|
|
379
394
|
if (Number.isFinite(at)) {
|
|
380
395
|
if (cur.firstAtMs === undefined || at < cur.firstAtMs)
|
|
@@ -382,7 +397,29 @@ export class ObservedReducer {
|
|
|
382
397
|
if (cur.lastAtMs === undefined || at > cur.lastAtMs)
|
|
383
398
|
cur.lastAtMs = at;
|
|
384
399
|
}
|
|
385
|
-
this.unhandled.set(
|
|
400
|
+
this.unhandled.set(name, cur);
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* 팔레트 물품에 **담고 있는 것**을 채운다 — 이미 아는 값을 덮지 않는다.
|
|
404
|
+
*
|
|
405
|
+
* 팔레트가 자기 품번·수량을 이미 들고 있으면(관측이 그렇게 말했으면) 담김 선언이 그것을 밀어내지
|
|
406
|
+
* 않는다. 비어 있던 자리만 채운다 — 「모른다」를 채우는 것이 이 작업의 목적이고, 아는 것을 바꾸는
|
|
407
|
+
* 것은 아니다.
|
|
408
|
+
*/
|
|
409
|
+
withContainedQuantity(cur, q, all) {
|
|
410
|
+
const parsed = q.epcClass ? parseEpc(q.epcClass) : undefined;
|
|
411
|
+
const declared = all.filter(x => typeof x.quantity === 'number');
|
|
412
|
+
return {
|
|
413
|
+
...cur,
|
|
414
|
+
gtin: cur.gtin ?? q.epcClass,
|
|
415
|
+
gtinKey: cur.gtinKey ?? parsed?.gtinKey,
|
|
416
|
+
/* 관측이 준 수량이 있으면 그대로 둔다. 없으면(팔레트 하나로 세어 `1` 이던 자리) 내용의 수량이 답이다. */
|
|
417
|
+
qty: cur.qty !== undefined && cur.qty !== 1 ? cur.qty : q.quantity,
|
|
418
|
+
uom: cur.uom ?? q.uom,
|
|
419
|
+
/* 단위가 여럿일 때만 목록을 든다(하나면 `qty`/`uom` 이 무손실로 들고 있다 — 위 `upsert` 와 같은 규율). */
|
|
420
|
+
quantities: cur.quantities ?? (declared.length >= 2 ? declared.map(x => ({ value: x.quantity, ...(x.uom ? { uom: x.uom } : {}) })) : undefined),
|
|
421
|
+
lot: cur.lot ?? parsed?.lot
|
|
422
|
+
};
|
|
386
423
|
}
|
|
387
424
|
applyEpcis(ev, envelope) {
|
|
388
425
|
/* 정정 선언이 붙은 이벤트는 **새 사실이 아니다** — 앞선 이벤트를 취소·수정하는 선언이다.
|
|
@@ -399,6 +436,46 @@ export class ObservedReducer {
|
|
|
399
436
|
return;
|
|
400
437
|
}
|
|
401
438
|
if (ev.type === 'AggregationEvent') {
|
|
439
|
+
/*
|
|
440
|
+
* ── 바코드 없이 **수량만** 실린 담김 (2026-08-19) ────────────────────────
|
|
441
|
+
* 실제 창고는 박스마다 바코드를 붙이지 않는다. 팔레트 하나에 「이 품번 40개」로 입고하는 것이
|
|
442
|
+
* 훨씬 흔하고, 표준도 그 자리를 둔다(`childQuantityList`: 클래스 + 수량).
|
|
443
|
+
*
|
|
444
|
+
* 그런데 여기는 **낱개 자식만** 반영했다. 그래서 미러는 그 팔레트를 `{epc, location, qty: 1}` 로
|
|
445
|
+
* 들었다 — 실측: 품번도 없고 40개도 없다. 화면에는 「팔레트 1개」만 보이고 그 안의 40개는 재고
|
|
446
|
+
* 집계에서 통째로 빠진다. 실 WMS 를 붙이면 바로 부딪히는 자리다.
|
|
447
|
+
*
|
|
448
|
+
* **어떻게 담나**: 시뮬 커널이 같은 입고를 다루는 방식과 맞춘다 — 그쪽은 팔레트 물품 자신에
|
|
449
|
+
* 품번과 수량을 싣는다(`items.set(epc, { epc, gtin, qty … })`). 그래서 여기서도 팔레트 물품에
|
|
450
|
+
* 채운다. 새 필드를 만들지 않으므로 **기존 재고 집계가 그대로 센다**(필드만 만들고 아무도 읽지
|
|
451
|
+
* 않는 상태를 이 프로젝트는 결함으로 본다). 두 구동이 같은 모양을 들게 되는 것이 덤이다.
|
|
452
|
+
*
|
|
453
|
+
* **못 하는 경우는 밝힌다**: 한 팔레트에 품번이 둘 이상이면 이 모양으로 표현할 수 없다(하나를
|
|
454
|
+
* 골라 채우면 나머지를 지우는 것이다). 그때는 채우지 않고 `unhandled` 로 센다 — 조용히 버리지
|
|
455
|
+
* 않는다. 그 어휘를 담으려면 상태에 「담고 있는 것들」이 따로 필요하고, 그것은 별 결정이다.
|
|
456
|
+
*/
|
|
457
|
+
if (ev.action === 'ADD' && ev.childQuantityList?.length) {
|
|
458
|
+
const classes = new Set(ev.childQuantityList.map(q => q.epcClass));
|
|
459
|
+
if (classes.size > 1) {
|
|
460
|
+
/* 여러 품번이 한 팔레트에 — 지금 상태로는 표현할 수 없다. 세어서 드러낸다. */
|
|
461
|
+
if (envelope)
|
|
462
|
+
this.noteUnhandled(envelope, 'aggregation-mixed-classes');
|
|
463
|
+
}
|
|
464
|
+
else {
|
|
465
|
+
const q = ev.childQuantityList[0];
|
|
466
|
+
const cur = this.items.get(ev.parentID);
|
|
467
|
+
/*
|
|
468
|
+
* **관측하지 못한 팔레트를 지어내지 않는다** — 담김은 *어디 있는지*를 말하지 않는다.
|
|
469
|
+
* 그 팔레트가 관측되면(ObjectEvent) 그때 위치와 함께 서고, 담김은 보류해 두었다가 붙인다.
|
|
470
|
+
*/
|
|
471
|
+
if (!cur)
|
|
472
|
+
this.pendingQuantities.set(ev.parentID, [...ev.childQuantityList]);
|
|
473
|
+
else {
|
|
474
|
+
this.items.set(cur.subLotId ?? cur.epc, this.withContainedQuantity(cur, q, ev.childQuantityList));
|
|
475
|
+
this.qtyAggregation.set(ev.parentID, [...ev.childQuantityList]);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
402
479
|
if (ev.action === 'ADD' && ev.childEPCs?.length) {
|
|
403
480
|
this.aggregation.set(ev.parentID, [...ev.childEPCs]);
|
|
404
481
|
/* 조립 관계를 물품에도 주입한다 — 예전에는 내부 맵에만 두고 밖으로 내보내지 않아, 3D 가
|
|
@@ -424,6 +501,16 @@ export class ObservedReducer {
|
|
|
424
501
|
this.pendingParent.delete(child);
|
|
425
502
|
}
|
|
426
503
|
this.aggregation.delete(ev.parentID);
|
|
504
|
+
/* 수량으로 담겼던 것을 풀면 그 팔레트는 **비게 된다** — 우리가 채운 것만 되돌린다(원래
|
|
505
|
+
팔레트 자신의 수량이었다면 건드리지 않는다). */
|
|
506
|
+
if (this.qtyAggregation.delete(ev.parentID)) {
|
|
507
|
+
const cur = this.items.get(ev.parentID);
|
|
508
|
+
if (cur) {
|
|
509
|
+
const { gtin, gtinKey, qty, uom, quantities, lot, ...rest } = cur;
|
|
510
|
+
this.items.set(cur.subLotId ?? cur.epc, { ...rest, qty: 1 });
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
this.pendingQuantities.delete(ev.parentID);
|
|
427
514
|
}
|
|
428
515
|
return;
|
|
429
516
|
}
|
|
@@ -512,7 +599,9 @@ export class ObservedReducer {
|
|
|
512
599
|
const parsedSelf = parseEpc(epc);
|
|
513
600
|
/* `gtin` 은 **클래스 URI 원문**이다(오더 매칭이 이 값을 쓴다 — 뜻을 바꾸면 조용히 안 맞는다).
|
|
514
601
|
* 파서로 뜯은 품번 키·로트는 **별도 필드**로 얹는다. */
|
|
515
|
-
|
|
602
|
+
/* 담김이 먼저 온 팔레트는 그 내용의 클래스가 곧 이 물품의 품번이다(시뮬이 같은 입고를 그렇게 든다). */
|
|
603
|
+
const pendingQty = this.pendingQuantities.get(epc);
|
|
604
|
+
const classUri = q?.epcClass ?? pendingQty?.[0]?.epcClass ?? (parsedSelf.instance ? undefined : epc);
|
|
516
605
|
return {
|
|
517
606
|
epc,
|
|
518
607
|
...(subLotId ? { subLotId } : {}),
|
|
@@ -521,8 +610,12 @@ export class ObservedReducer {
|
|
|
521
610
|
location: patch.location ?? cur?.location ?? '',
|
|
522
611
|
disposition: patch.disposition ?? cur?.disposition,
|
|
523
612
|
parent: cur?.parent ?? this.pendingParent.get(epc),
|
|
524
|
-
|
|
525
|
-
|
|
613
|
+
/*
|
|
614
|
+
* 관측이 수량을 말하지 않았는데 **담김이 먼저 와 있었다면** 그 내용이 답이다(바코드 없이 수량만
|
|
615
|
+
* 실린 팔레트). 보류해 둔 것을 여기서 붙인다 — 위 `parent` 와 같은 규율이다.
|
|
616
|
+
*/
|
|
617
|
+
qty: q?.quantity ?? this.pendingQuantities.get(epc)?.[0]?.quantity ?? cur?.qty,
|
|
618
|
+
uom: q?.uom ?? this.pendingQuantities.get(epc)?.[0]?.uom ?? cur?.uom,
|
|
526
619
|
/*
|
|
527
620
|
* 선언된 수량 전부 — 값이 없는 항목은 담지 않는다(모름을 0 으로 만들지 않는다).
|
|
528
621
|
*
|
|
@@ -656,6 +749,9 @@ export class ObservedReducer {
|
|
|
656
749
|
items: [...this.items.values()].map(i => ({ ...i })),
|
|
657
750
|
aggregation: [...this.aggregation.entries()].map(([parent, children]) => ({ parent, children: [...children] })),
|
|
658
751
|
pendingParent: [...this.pendingParent.entries()].map(([child, parent]) => ({ child, parent })),
|
|
752
|
+
/* 수량으로 담긴 것도 이어받는다 — 되풀어 주지 않는 축이다(원천은 담김을 다시 말해 주지 않는다). */
|
|
753
|
+
qtyAggregation: [...this.qtyAggregation.entries()].map(([parent, quantities]) => ({ parent, quantities })),
|
|
754
|
+
pendingQuantities: [...this.pendingQuantities.entries()].map(([parent, quantities]) => ({ parent, quantities })),
|
|
659
755
|
tasks: [...this.tasks.values()].map(t => ({ ...t })),
|
|
660
756
|
equipment: [...this.equipment.values()].map(m => ({ ...m })),
|
|
661
757
|
persons: [...this.persons.values()].map(x => ({ ...x })),
|
|
@@ -677,6 +773,8 @@ export class ObservedReducer {
|
|
|
677
773
|
this.items = new Map((cp?.items ?? []).map(i => [i.epc, { ...i }]));
|
|
678
774
|
this.aggregation = new Map((cp?.aggregation ?? []).map(a => [a.parent, [...a.children]]));
|
|
679
775
|
this.pendingParent = new Map((cp?.pendingParent ?? []).map(x => [x.child, x.parent]));
|
|
776
|
+
this.qtyAggregation = new Map((cp?.qtyAggregation ?? []).map(x => [x.parent, x.quantities]));
|
|
777
|
+
this.pendingQuantities = new Map((cp?.pendingQuantities ?? []).map(x => [x.parent, x.quantities]));
|
|
680
778
|
this.tasks = new Map((cp?.tasks ?? []).map(t => [t.id, { ...t }]));
|
|
681
779
|
this.equipment = new Map((cp?.equipment ?? []).map(m => [m.id, { ...m }]));
|
|
682
780
|
this.persons = new Map((cp?.persons ?? []).map(x => [x.id, { ...x }]));
|
package/dist-cjs/index.cjs
CHANGED
|
@@ -1091,6 +1091,15 @@ var ObservedReducer = class {
|
|
|
1091
1091
|
/** 아직 관측되지 않은 자식의 담김 — 물품을 지어내지 않고 보류했다가 등장할 때 붙인다. */
|
|
1092
1092
|
pendingParent = /* @__PURE__ */ new Map();
|
|
1093
1093
|
// 자식 EPC → 부모(물류단위)
|
|
1094
|
+
/**
|
|
1095
|
+
* 팔레트가 **담고 있는 것**(클래스 + 수량) — 바코드 없이 수량만 실린 담김.
|
|
1096
|
+
*
|
|
1097
|
+
* · `qtyAggregation` — 우리가 그 팔레트 물품에 채워 넣은 내용(풀릴 때 되돌릴 근거).
|
|
1098
|
+
* · `pendingQuantities` — 담김이 먼저 오고 그 팔레트를 아직 관측하지 못한 경우. 물품을 지어내지
|
|
1099
|
+
* 않고 보류해 두었다가, 관측되는 순간 붙인다(`pendingParent` 와 같은 규율).
|
|
1100
|
+
*/
|
|
1101
|
+
qtyAggregation = /* @__PURE__ */ new Map();
|
|
1102
|
+
pendingQuantities = /* @__PURE__ */ new Map();
|
|
1094
1103
|
tasks = /* @__PURE__ */ new Map();
|
|
1095
1104
|
equipment = /* @__PURE__ */ new Map();
|
|
1096
1105
|
persons = /* @__PURE__ */ new Map();
|
|
@@ -1381,16 +1390,44 @@ var ObservedReducer = class {
|
|
|
1381
1390
|
this.noteUnhandled(e);
|
|
1382
1391
|
}
|
|
1383
1392
|
}
|
|
1384
|
-
/**
|
|
1385
|
-
|
|
1393
|
+
/**
|
|
1394
|
+
* 반영하지 못한 사건을 종류별로 센다 — 처음·마지막 시각을 함께 남겨 「언제부터」에 답한다.
|
|
1395
|
+
*
|
|
1396
|
+
* `key` 를 주면 그 이름으로 센다: 종류 전체를 못 다룬 것과 **그 종류의 어떤 모양만** 못 다룬 것은
|
|
1397
|
+
* 다른 사실이다(예: 담김은 반영하는데 품번이 섞인 팔레트만 못 담는다).
|
|
1398
|
+
*/
|
|
1399
|
+
noteUnhandled(e, key) {
|
|
1386
1400
|
const at = Date.parse(String(e.eventTime ?? ""));
|
|
1387
|
-
const
|
|
1401
|
+
const name = key ?? e.eventType;
|
|
1402
|
+
const cur = this.unhandled.get(name) ?? { count: 0 };
|
|
1388
1403
|
cur.count++;
|
|
1389
1404
|
if (Number.isFinite(at)) {
|
|
1390
1405
|
if (cur.firstAtMs === void 0 || at < cur.firstAtMs) cur.firstAtMs = at;
|
|
1391
1406
|
if (cur.lastAtMs === void 0 || at > cur.lastAtMs) cur.lastAtMs = at;
|
|
1392
1407
|
}
|
|
1393
|
-
this.unhandled.set(
|
|
1408
|
+
this.unhandled.set(name, cur);
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* 팔레트 물품에 **담고 있는 것**을 채운다 — 이미 아는 값을 덮지 않는다.
|
|
1412
|
+
*
|
|
1413
|
+
* 팔레트가 자기 품번·수량을 이미 들고 있으면(관측이 그렇게 말했으면) 담김 선언이 그것을 밀어내지
|
|
1414
|
+
* 않는다. 비어 있던 자리만 채운다 — 「모른다」를 채우는 것이 이 작업의 목적이고, 아는 것을 바꾸는
|
|
1415
|
+
* 것은 아니다.
|
|
1416
|
+
*/
|
|
1417
|
+
withContainedQuantity(cur, q, all) {
|
|
1418
|
+
const parsed = q.epcClass ? parseEpc(q.epcClass) : void 0;
|
|
1419
|
+
const declared = all.filter((x) => typeof x.quantity === "number");
|
|
1420
|
+
return {
|
|
1421
|
+
...cur,
|
|
1422
|
+
gtin: cur.gtin ?? q.epcClass,
|
|
1423
|
+
gtinKey: cur.gtinKey ?? parsed?.gtinKey,
|
|
1424
|
+
/* 관측이 준 수량이 있으면 그대로 둔다. 없으면(팔레트 하나로 세어 `1` 이던 자리) 내용의 수량이 답이다. */
|
|
1425
|
+
qty: cur.qty !== void 0 && cur.qty !== 1 ? cur.qty : q.quantity,
|
|
1426
|
+
uom: cur.uom ?? q.uom,
|
|
1427
|
+
/* 단위가 여럿일 때만 목록을 든다(하나면 `qty`/`uom` 이 무손실로 들고 있다 — 위 `upsert` 와 같은 규율). */
|
|
1428
|
+
quantities: cur.quantities ?? (declared.length >= 2 ? declared.map((x) => ({ value: x.quantity, ...x.uom ? { uom: x.uom } : {} })) : void 0),
|
|
1429
|
+
lot: cur.lot ?? parsed?.lot
|
|
1430
|
+
};
|
|
1394
1431
|
}
|
|
1395
1432
|
applyEpcis(ev, envelope) {
|
|
1396
1433
|
if (ev.errorDeclaration) {
|
|
@@ -1403,6 +1440,20 @@ var ObservedReducer = class {
|
|
|
1403
1440
|
return;
|
|
1404
1441
|
}
|
|
1405
1442
|
if (ev.type === "AggregationEvent") {
|
|
1443
|
+
if (ev.action === "ADD" && ev.childQuantityList?.length) {
|
|
1444
|
+
const classes = new Set(ev.childQuantityList.map((q2) => q2.epcClass));
|
|
1445
|
+
if (classes.size > 1) {
|
|
1446
|
+
if (envelope) this.noteUnhandled(envelope, "aggregation-mixed-classes");
|
|
1447
|
+
} else {
|
|
1448
|
+
const q2 = ev.childQuantityList[0];
|
|
1449
|
+
const cur = this.items.get(ev.parentID);
|
|
1450
|
+
if (!cur) this.pendingQuantities.set(ev.parentID, [...ev.childQuantityList]);
|
|
1451
|
+
else {
|
|
1452
|
+
this.items.set(cur.subLotId ?? cur.epc, this.withContainedQuantity(cur, q2, ev.childQuantityList));
|
|
1453
|
+
this.qtyAggregation.set(ev.parentID, [...ev.childQuantityList]);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1406
1457
|
if (ev.action === "ADD" && ev.childEPCs?.length) {
|
|
1407
1458
|
this.aggregation.set(ev.parentID, [...ev.childEPCs]);
|
|
1408
1459
|
for (const child of ev.childEPCs) {
|
|
@@ -1420,6 +1471,14 @@ var ObservedReducer = class {
|
|
|
1420
1471
|
this.pendingParent.delete(child);
|
|
1421
1472
|
}
|
|
1422
1473
|
this.aggregation.delete(ev.parentID);
|
|
1474
|
+
if (this.qtyAggregation.delete(ev.parentID)) {
|
|
1475
|
+
const cur = this.items.get(ev.parentID);
|
|
1476
|
+
if (cur) {
|
|
1477
|
+
const { gtin, gtinKey, qty, uom, quantities, lot, ...rest } = cur;
|
|
1478
|
+
this.items.set(cur.subLotId ?? cur.epc, { ...rest, qty: 1 });
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
this.pendingQuantities.delete(ev.parentID);
|
|
1423
1482
|
}
|
|
1424
1483
|
return;
|
|
1425
1484
|
}
|
|
@@ -1477,7 +1536,8 @@ var ObservedReducer = class {
|
|
|
1477
1536
|
const cur = this.items.get(subLotId ?? epc);
|
|
1478
1537
|
const parsedClass = q?.epcClass ? parseEpc(q.epcClass) : void 0;
|
|
1479
1538
|
const parsedSelf = parseEpc(epc);
|
|
1480
|
-
const
|
|
1539
|
+
const pendingQty = this.pendingQuantities.get(epc);
|
|
1540
|
+
const classUri = q?.epcClass ?? pendingQty?.[0]?.epcClass ?? (parsedSelf.instance ? void 0 : epc);
|
|
1481
1541
|
return {
|
|
1482
1542
|
epc,
|
|
1483
1543
|
...subLotId ? { subLotId } : {},
|
|
@@ -1486,8 +1546,12 @@ var ObservedReducer = class {
|
|
|
1486
1546
|
location: patch.location ?? cur?.location ?? "",
|
|
1487
1547
|
disposition: patch.disposition ?? cur?.disposition,
|
|
1488
1548
|
parent: cur?.parent ?? this.pendingParent.get(epc),
|
|
1489
|
-
|
|
1490
|
-
|
|
1549
|
+
/*
|
|
1550
|
+
* 관측이 수량을 말하지 않았는데 **담김이 먼저 와 있었다면** 그 내용이 답이다(바코드 없이 수량만
|
|
1551
|
+
* 실린 팔레트). 보류해 둔 것을 여기서 붙인다 — 위 `parent` 와 같은 규율이다.
|
|
1552
|
+
*/
|
|
1553
|
+
qty: q?.quantity ?? this.pendingQuantities.get(epc)?.[0]?.quantity ?? cur?.qty,
|
|
1554
|
+
uom: q?.uom ?? this.pendingQuantities.get(epc)?.[0]?.uom ?? cur?.uom,
|
|
1491
1555
|
/*
|
|
1492
1556
|
* 선언된 수량 전부 — 값이 없는 항목은 담지 않는다(모름을 0 으로 만들지 않는다).
|
|
1493
1557
|
*
|
|
@@ -1615,6 +1679,9 @@ var ObservedReducer = class {
|
|
|
1615
1679
|
items: [...this.items.values()].map((i) => ({ ...i })),
|
|
1616
1680
|
aggregation: [...this.aggregation.entries()].map(([parent, children]) => ({ parent, children: [...children] })),
|
|
1617
1681
|
pendingParent: [...this.pendingParent.entries()].map(([child, parent]) => ({ child, parent })),
|
|
1682
|
+
/* 수량으로 담긴 것도 이어받는다 — 되풀어 주지 않는 축이다(원천은 담김을 다시 말해 주지 않는다). */
|
|
1683
|
+
qtyAggregation: [...this.qtyAggregation.entries()].map(([parent, quantities]) => ({ parent, quantities })),
|
|
1684
|
+
pendingQuantities: [...this.pendingQuantities.entries()].map(([parent, quantities]) => ({ parent, quantities })),
|
|
1618
1685
|
tasks: [...this.tasks.values()].map((t) => ({ ...t })),
|
|
1619
1686
|
equipment: [...this.equipment.values()].map((m) => ({ ...m })),
|
|
1620
1687
|
persons: [...this.persons.values()].map((x) => ({ ...x })),
|
|
@@ -1636,6 +1703,8 @@ var ObservedReducer = class {
|
|
|
1636
1703
|
this.items = new Map((cp?.items ?? []).map((i) => [i.epc, { ...i }]));
|
|
1637
1704
|
this.aggregation = new Map((cp?.aggregation ?? []).map((a) => [a.parent, [...a.children]]));
|
|
1638
1705
|
this.pendingParent = new Map((cp?.pendingParent ?? []).map((x) => [x.child, x.parent]));
|
|
1706
|
+
this.qtyAggregation = new Map((cp?.qtyAggregation ?? []).map((x) => [x.parent, x.quantities]));
|
|
1707
|
+
this.pendingQuantities = new Map((cp?.pendingQuantities ?? []).map((x) => [x.parent, x.quantities]));
|
|
1639
1708
|
this.tasks = new Map((cp?.tasks ?? []).map((t) => [t.id, { ...t }]));
|
|
1640
1709
|
this.equipment = new Map((cp?.equipment ?? []).map((m) => [m.id, { ...m }]));
|
|
1641
1710
|
this.persons = new Map((cp?.persons ?? []).map((x) => [x.id, { ...x }]));
|
|
@@ -3916,6 +3985,11 @@ var FlowEngine = class {
|
|
|
3916
3985
|
/* 스냅샷이 **델타보다 가난하면 안 된다** — 예전에는 소요·남은 시간을 빼고 내보내서, 이 스냅샷으로
|
|
3917
3986
|
* 다른 커널을 주입하면(hydrateObserved) 진행 중이던 작업을 이어 굴릴 수 없었다(미러 스냅샷은
|
|
3918
3987
|
* 델타에서 왔으므로 갖고 있었다 — 같은 계약을 두 구동이 다르게 채우던 자리). */
|
|
3988
|
+
/* 관측이 담지 못한 사실을 그대로 내보낸다 — 리듀서가 세고 있어도 여기서 떨어뜨리면 아무도 못 본다. */
|
|
3989
|
+
...(() => {
|
|
3990
|
+
const u = this.observer?.snapshot?.().unhandled;
|
|
3991
|
+
return u?.length ? { unhandled: u } : {};
|
|
3992
|
+
})(),
|
|
3919
3993
|
tasks: [...this.tasks.values()].map((t) => ({
|
|
3920
3994
|
id: t.id,
|
|
3921
3995
|
kind: t.kind,
|
package/package.json
CHANGED