@bpmn-nova/studio 0.3.5-preview → 0.3.7-preview
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/README.md +16 -367
- package/dist/modules/export-svg/render.js +101 -4
- package/dist/modules/renderer-svg/index.js +1 -0
- package/dist/modules/runtime/diagnostics.js +22 -0
- package/dist/modules/runtime/index.d.ts +38 -4
- package/dist/modules/runtime/index.js +272 -80
- package/dist/modules/runtime/time.js +41 -0
- package/dist/modules/runtime/visits.js +167 -0
- package/dist/modules/viewer/index.js +139 -20
- package/dist/modules/viewer/runtime-avatar.js +49 -0
- package/dist/modules/viewer/runtime-trace.js +33 -30
- package/dist/modules/viewer/timeline.js +12 -3
- package/dist/shell.js +1 -1
- package/dist/styles.css +5 -0
- package/llms-full.txt +3396 -1747
- package/llms.txt +6 -4
- package/package.json +1 -1
|
@@ -51,9 +51,27 @@ export interface RuntimeApprovalActionPresentation extends Omit<RuntimeApprovalA
|
|
|
51
51
|
assetCount: number
|
|
52
52
|
order: number
|
|
53
53
|
}
|
|
54
|
+
export interface RuntimeDiagnostic {
|
|
55
|
+
code: string
|
|
56
|
+
level: 'error' | 'warning' | 'info'
|
|
57
|
+
message: string
|
|
58
|
+
fieldPath?: string
|
|
59
|
+
elementId?: string
|
|
60
|
+
activityId?: string
|
|
61
|
+
visitId?: string
|
|
62
|
+
actionId?: string
|
|
63
|
+
transitionId?: string
|
|
64
|
+
edgeId?: string
|
|
65
|
+
}
|
|
66
|
+
export interface RuntimeInstant {
|
|
67
|
+
instant: number | null
|
|
68
|
+
raw: string
|
|
69
|
+
kind: 'offset' | 'z' | 'naive' | 'invalid' | 'missing'
|
|
70
|
+
}
|
|
54
71
|
export interface RuntimeApprovalActionSummary {
|
|
55
72
|
actions: RuntimeApprovalActionPresentation[]
|
|
56
73
|
latestAction: RuntimeApprovalActionPresentation | null
|
|
74
|
+
latestNonEmptyComment: RuntimeApprovalActionPresentation | null
|
|
57
75
|
actionText: string
|
|
58
76
|
actionSummary: string
|
|
59
77
|
imageCount: number
|
|
@@ -70,6 +88,7 @@ export interface ActivityInstance {
|
|
|
70
88
|
assigneeId?: string
|
|
71
89
|
participant?: RuntimeParticipant
|
|
72
90
|
visitId?: string
|
|
91
|
+
supersedesVisitId?: string
|
|
73
92
|
multiInstanceId?: string
|
|
74
93
|
approvalMode?: 'single' | 'all' | 'any'
|
|
75
94
|
multiInstanceMode?: 'parallel' | 'sequential'
|
|
@@ -92,6 +111,7 @@ export interface RuntimeTransition {
|
|
|
92
111
|
state?: 'active' | 'resolved'
|
|
93
112
|
resolvedAt?: string
|
|
94
113
|
resolvedByActivityId?: string
|
|
114
|
+
invalidatedElementIds?: string[]
|
|
95
115
|
invalidatedActivityIds?: string[]
|
|
96
116
|
invalidatedEdgeIds?: string[]
|
|
97
117
|
}
|
|
@@ -120,10 +140,18 @@ export interface RuntimeTransitionPresentation extends RuntimeTransition {
|
|
|
120
140
|
label: string
|
|
121
141
|
latest: boolean
|
|
122
142
|
issues: string[]
|
|
143
|
+
invalidatedElementIds: string[]
|
|
123
144
|
invalidatedActivityIds: string[]
|
|
124
145
|
invalidatedEdgeIds: string[]
|
|
125
146
|
action: RuntimeApprovalActionPresentation | null
|
|
126
147
|
}
|
|
148
|
+
export interface ActivityVisitState {
|
|
149
|
+
status: ActivityStatus
|
|
150
|
+
records: ActivityInstance[]
|
|
151
|
+
latestRecords: ActivityInstance[]
|
|
152
|
+
visits: Array<{ id: string; round: number; records: ActivityInstance[]; effective?: boolean; superseded?: boolean }>
|
|
153
|
+
effectiveVisits: Array<{ id: string; round: number; records: ActivityInstance[]; effective?: boolean; superseded?: boolean }>
|
|
154
|
+
}
|
|
127
155
|
export interface NodeRuntimePresentation extends RuntimeApprovalActionSummary {
|
|
128
156
|
elementId: string
|
|
129
157
|
status: ActivityStatus | 'rejected'
|
|
@@ -141,13 +169,15 @@ export interface NodeRuntimePresentation extends RuntimeApprovalActionSummary {
|
|
|
141
169
|
round: number
|
|
142
170
|
records: ActivityInstance[]
|
|
143
171
|
latestRecords: ActivityInstance[]
|
|
144
|
-
visits: Array<{ id: string; round: number; records: ActivityInstance[] } & RuntimeApprovalActionSummary>
|
|
172
|
+
visits: Array<{ id: string; round: number; records: ActivityInstance[]; effective: boolean; superseded: boolean } & RuntimeApprovalActionSummary>
|
|
173
|
+
effectiveVisits: Array<{ id: string; round: number; records: ActivityInstance[]; effective: boolean; superseded: boolean } & RuntimeApprovalActionSummary>
|
|
145
174
|
transitions: RuntimeTransitionPresentation[]
|
|
146
175
|
isReentry: boolean
|
|
147
176
|
hasDetails: boolean
|
|
148
177
|
}
|
|
149
178
|
export interface RuntimePresentation {
|
|
150
179
|
runtime: ProcessInstanceSnapshot
|
|
180
|
+
diagnostics: readonly RuntimeDiagnostic[]
|
|
151
181
|
getNode(elementId: string): NodeRuntimePresentation
|
|
152
182
|
getEdge(edgeId: string): { edgeId: string; status: ActivityStatus; visited: boolean; historicallyVisited: boolean; superseded: boolean }
|
|
153
183
|
getAction(actionId: string): RuntimeApprovalActionPresentation | null
|
|
@@ -161,9 +191,13 @@ export interface RuntimeAppearanceLike {
|
|
|
161
191
|
resolveTransition(transition?: { type?: string }): { tone: string }
|
|
162
192
|
}
|
|
163
193
|
|
|
164
|
-
export function
|
|
165
|
-
export function
|
|
166
|
-
export function
|
|
194
|
+
export function parseRuntimeInstant(value?: string | null): RuntimeInstant
|
|
195
|
+
export function formatRuntimeInstant(value?: string | RuntimeInstant | null): string
|
|
196
|
+
export function compareRuntimeOrder(left: { instant?: number | null; index?: number } | string | null | undefined, right: { instant?: number | null; index?: number } | string | null | undefined): number
|
|
197
|
+
export function normalizeRuntime(snapshot?: Partial<ProcessInstanceSnapshot> | null, options?: { diagnostics?: RuntimeDiagnostic[]; model?: ProcessModel | null }): ProcessInstanceSnapshot
|
|
198
|
+
export function activityState(runtime: ProcessInstanceSnapshot | null | undefined, elementId: string, options?: { diagnostics?: RuntimeDiagnostic[] }): ActivityVisitState
|
|
199
|
+
export function inspectRuntime(snapshot?: Partial<ProcessInstanceSnapshot> | null, options?: { model?: ProcessModel | null; appearance?: RuntimeAppearanceLike | null }): { runtime: ProcessInstanceSnapshot; diagnostics: readonly RuntimeDiagnostic[] }
|
|
200
|
+
export function createRuntimePresentation(context: { model: ProcessModel; runtime?: ProcessInstanceSnapshot | null; appearance?: RuntimeAppearanceLike | null; diagnostics?: RuntimeDiagnostic[] }): RuntimePresentation
|
|
167
201
|
export function demoRuntime(): ProcessInstanceSnapshot
|
|
168
202
|
|
|
169
203
|
export type RuntimeElement = BpmnNode | BpmnEdge
|
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { parseRuntimeInstant, formatRuntimeInstant, compareRuntimeOrder, runtimeInstant } from './time.js';
|
|
2
|
+
import { pushDiagnostic } from './diagnostics.js';
|
|
3
|
+
import { groupVisits, markEffectiveVisits, resolveEffectiveStatus, resolveVisitStatus } from './visits.js';
|
|
4
|
+
|
|
5
|
+
export { parseRuntimeInstant, formatRuntimeInstant, compareRuntimeOrder } from './time.js';
|
|
6
|
+
export { groupVisits, resolveVisitStatus, resolveEffectiveStatus } from './visits.js';
|
|
7
|
+
|
|
1
8
|
const TERMINAL_PROCESS_STATUSES = new Set(['completed', 'terminated']);
|
|
2
9
|
const ABNORMAL_TRANSITION_TYPES = new Set(['reject', 'return']);
|
|
3
10
|
const APPROVAL_ACTION_LABELS = {
|
|
@@ -14,16 +21,21 @@ function normalizeParticipant(record = {}) {
|
|
|
14
21
|
return null;
|
|
15
22
|
}
|
|
16
23
|
|
|
24
|
+
function stampInstant(value) {
|
|
25
|
+
return runtimeInstant(value);
|
|
26
|
+
}
|
|
27
|
+
|
|
17
28
|
function recordOrder(record, index) {
|
|
18
|
-
|
|
19
|
-
const parsed = stamp ? Date.parse(stamp.replace(' ', 'T')) : Number.NaN;
|
|
20
|
-
return Number.isFinite(parsed) ? parsed : index;
|
|
29
|
+
return stampInstant(record.startTime || record.endTime);
|
|
21
30
|
}
|
|
22
31
|
|
|
23
|
-
function runtimeOrder(value
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
function runtimeOrder(value) {
|
|
33
|
+
return stampInstant(value?.occurredAt || value?.time || value?.endTime || value?.startTime);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isAfter(left, right) {
|
|
37
|
+
if (left == null || right == null) return false;
|
|
38
|
+
return left > right;
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
function normalizeRuntimeAsset(asset) {
|
|
@@ -107,12 +119,14 @@ function approvalActionLabel(action) {
|
|
|
107
119
|
}
|
|
108
120
|
|
|
109
121
|
function summarizeApprovalActions(actions = []) {
|
|
110
|
-
const latestAction =
|
|
122
|
+
const latestAction = actions.at(-1) || null;
|
|
123
|
+
const latestNonEmptyComment = [...actions].reverse().find((action) => action.plainText || action.assetCount) || null;
|
|
111
124
|
const assetSummary = latestAction
|
|
112
125
|
? [latestAction.imageCount ? `${latestAction.imageCount} 张图片` : '', latestAction.fileCount ? `${latestAction.fileCount} 个附件` : ''].filter(Boolean).join(' · ')
|
|
113
126
|
: '';
|
|
114
127
|
return {
|
|
115
128
|
latestAction,
|
|
129
|
+
latestNonEmptyComment,
|
|
116
130
|
actionText: latestAction?.plainText || '',
|
|
117
131
|
actionSummary: latestAction ? [latestAction.label, latestAction.plainText, assetSummary].filter(Boolean).join(' · ') : '',
|
|
118
132
|
imageCount: actions.reduce((sum, action) => sum + action.imageCount, 0),
|
|
@@ -121,31 +135,6 @@ function summarizeApprovalActions(actions = []) {
|
|
|
121
135
|
};
|
|
122
136
|
}
|
|
123
137
|
|
|
124
|
-
function groupVisits(records = []) {
|
|
125
|
-
const groups = new Map();
|
|
126
|
-
records.forEach((record, index) => {
|
|
127
|
-
const key = record.visitId
|
|
128
|
-
? `visit:${record.visitId}`
|
|
129
|
-
: record.multiInstanceId
|
|
130
|
-
? `multi:${record.multiInstanceId}`
|
|
131
|
-
: `record:${record.id || index}`;
|
|
132
|
-
if (!groups.has(key)) groups.set(key, { id: record.visitId || record.multiInstanceId || record.id || String(index), records: [], order: recordOrder(record, index) });
|
|
133
|
-
const visit = groups.get(key);
|
|
134
|
-
visit.records.push(record);
|
|
135
|
-
visit.order = Math.max(visit.order, recordOrder(record, index));
|
|
136
|
-
});
|
|
137
|
-
return [...groups.values()].sort((a, b) => a.order - b.order).map((visit, index) => ({ ...visit, round: index + 1 }));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
function resolveVisitStatus(records = []) {
|
|
141
|
-
if (!records.length) return 'idle';
|
|
142
|
-
if (records.some((item) => item.status === 'failed')) return 'failed';
|
|
143
|
-
if (records.some((item) => item.status === 'active')) return 'active';
|
|
144
|
-
if (records.every((item) => item.status === 'skipped')) return 'skipped';
|
|
145
|
-
if (records.some((item) => item.status === 'cancelled') && !records.some((item) => item.status === 'completed')) return 'cancelled';
|
|
146
|
-
return 'completed';
|
|
147
|
-
}
|
|
148
|
-
|
|
149
138
|
function uniqueParticipants(records = []) {
|
|
150
139
|
const seen = new Set();
|
|
151
140
|
const result = [];
|
|
@@ -180,16 +169,146 @@ function baseStatusLabel(status) {
|
|
|
180
169
|
}[status] || status;
|
|
181
170
|
}
|
|
182
171
|
|
|
183
|
-
function transitionOrder(transition
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
172
|
+
function transitionOrder(transition) {
|
|
173
|
+
return stampInstant(transition.occurredAt || transition.time);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function edgeVisitOrder(visit) {
|
|
177
|
+
return stampInstant(visit.occurredAt || visit.time);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function collectIdentityDiagnostics(runtime, { model = null, diagnostics }) {
|
|
181
|
+
const activityIds = new Set();
|
|
182
|
+
const actionIds = new Set();
|
|
183
|
+
const transitionIds = new Set();
|
|
184
|
+
const edgeVisitIds = new Set();
|
|
185
|
+
const visitOwners = new Map();
|
|
186
|
+
const nodeIds = new Set((model?.nodes || []).map((node) => node.id));
|
|
187
|
+
const edgeIds = new Set((model?.edges || []).map((edge) => edge.id));
|
|
188
|
+
|
|
189
|
+
runtime.activities.forEach((record, index) => {
|
|
190
|
+
const fieldPath = `activities[${index}]`;
|
|
191
|
+
if (record.id) {
|
|
192
|
+
if (activityIds.has(record.id)) pushDiagnostic(diagnostics, { code: 'duplicate-activity-id', level: 'error', message: '工作项 ID 重复。', fieldPath, activityId: record.id, elementId: record.elementId });
|
|
193
|
+
activityIds.add(record.id);
|
|
194
|
+
}
|
|
195
|
+
if (record.visitId) {
|
|
196
|
+
const owner = visitOwners.get(record.visitId);
|
|
197
|
+
if (owner && owner !== record.elementId) {
|
|
198
|
+
pushDiagnostic(diagnostics, { code: 'visit-element-mismatch', level: 'error', message: '同一 visitId 关联了不同 BPMN 元素。', fieldPath, visitId: record.visitId, elementId: record.elementId });
|
|
199
|
+
}
|
|
200
|
+
visitOwners.set(record.visitId, record.elementId);
|
|
201
|
+
}
|
|
202
|
+
['startTime', 'endTime'].forEach((field) => {
|
|
203
|
+
const parsed = parseRuntimeInstant(record[field]);
|
|
204
|
+
if (parsed.kind === 'invalid') pushDiagnostic(diagnostics, { code: 'invalid-time', level: 'warning', message: '时间无法解析。', fieldPath: `${fieldPath}.${field}`, activityId: record.id, elementId: record.elementId });
|
|
205
|
+
else if (parsed.kind === 'naive') pushDiagnostic(diagnostics, { code: 'naive-time', level: 'info', message: '时间缺少时区,比较结果可能因环境而异。', fieldPath: `${fieldPath}.${field}`, activityId: record.id, elementId: record.elementId });
|
|
206
|
+
});
|
|
207
|
+
if (model && record.elementId && !nodeIds.has(record.elementId)) {
|
|
208
|
+
pushDiagnostic(diagnostics, { code: 'unknown-element', level: 'error', message: '工作项引用了不存在的 BPMN 元素。', fieldPath: `${fieldPath}.elementId`, activityId: record.id, elementId: record.elementId });
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
runtime.actions.forEach((action, index) => {
|
|
213
|
+
const fieldPath = `actions[${index}]`;
|
|
214
|
+
if (actionIds.has(action.id)) pushDiagnostic(diagnostics, { code: 'duplicate-action-id', level: 'error', message: '操作 ID 重复。', fieldPath, actionId: action.id, elementId: action.elementId });
|
|
215
|
+
actionIds.add(action.id);
|
|
216
|
+
if (action.activityId && !activityIds.has(action.activityId) && !String(action.id).startsWith('legacy-')) {
|
|
217
|
+
pushDiagnostic(diagnostics, { code: 'unknown-activity', level: 'error', message: '操作引用了不存在的工作项。', fieldPath: `${fieldPath}.activityId`, actionId: action.id, activityId: action.activityId, elementId: action.elementId });
|
|
218
|
+
}
|
|
219
|
+
const record = runtime.activities.find((item) => item.id && item.id === action.activityId);
|
|
220
|
+
if (record && ((action.elementId && record.elementId !== action.elementId) || (action.visitId && record.visitId && record.visitId !== action.visitId))) {
|
|
221
|
+
pushDiagnostic(diagnostics, { code: 'action-identity-mismatch', level: 'error', message: '操作与工作项的元素或 Visit 不一致。', fieldPath, actionId: action.id, activityId: action.activityId, elementId: action.elementId, visitId: action.visitId });
|
|
222
|
+
}
|
|
223
|
+
const parsed = parseRuntimeInstant(action.occurredAt);
|
|
224
|
+
if (parsed.kind === 'invalid') pushDiagnostic(diagnostics, { code: 'invalid-time', level: 'warning', message: '操作时间无法解析。', fieldPath: `${fieldPath}.occurredAt`, actionId: action.id });
|
|
225
|
+
else if (parsed.kind === 'naive') pushDiagnostic(diagnostics, { code: 'naive-time', level: 'info', message: '操作时间缺少时区。', fieldPath: `${fieldPath}.occurredAt`, actionId: action.id });
|
|
226
|
+
else if (parsed.kind === 'missing' && !String(action.id).startsWith('legacy-')) {
|
|
227
|
+
pushDiagnostic(diagnostics, { code: 'missing-time', level: 'warning', message: '操作缺少时间,排序仅用于展示。', fieldPath: `${fieldPath}.occurredAt`, actionId: action.id });
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
runtime.transitions.forEach((transition, index) => {
|
|
232
|
+
const fieldPath = `transitions[${index}]`;
|
|
233
|
+
const id = transition.id || `runtime-transition-${index + 1}`;
|
|
234
|
+
if (transitionIds.has(id)) pushDiagnostic(diagnostics, { code: 'duplicate-transition-id', level: 'error', message: '跳转 ID 重复。', fieldPath, transitionId: id });
|
|
235
|
+
transitionIds.add(id);
|
|
236
|
+
if (transition.sourceActivityId && !activityIds.has(transition.sourceActivityId)) {
|
|
237
|
+
pushDiagnostic(diagnostics, { code: 'unknown-activity', level: 'error', message: '跳转引用了不存在的来源工作项。', fieldPath: `${fieldPath}.sourceActivityId`, transitionId: id, activityId: transition.sourceActivityId, elementId: transition.sourceElementId });
|
|
238
|
+
}
|
|
239
|
+
if (transition.actionId && !actionIds.has(transition.actionId) && !String(transition.actionId).startsWith('legacy-')) {
|
|
240
|
+
pushDiagnostic(diagnostics, { code: 'unknown-action', level: 'error', message: '跳转引用了不存在的操作。', fieldPath: `${fieldPath}.actionId`, transitionId: id, actionId: transition.actionId });
|
|
241
|
+
}
|
|
242
|
+
if (model && transition.sourceElementId && !nodeIds.has(transition.sourceElementId)) {
|
|
243
|
+
pushDiagnostic(diagnostics, { code: 'unknown-element', level: 'error', message: '跳转来源元素不存在。', fieldPath: `${fieldPath}.sourceElementId`, transitionId: id, elementId: transition.sourceElementId });
|
|
244
|
+
}
|
|
245
|
+
if (model && transition.targetElementId && !nodeIds.has(transition.targetElementId)) {
|
|
246
|
+
pushDiagnostic(diagnostics, { code: 'unknown-element', level: 'error', message: '跳转目标元素不存在。', fieldPath: `${fieldPath}.targetElementId`, transitionId: id, elementId: transition.targetElementId });
|
|
247
|
+
}
|
|
248
|
+
const parsed = parseRuntimeInstant(transition.occurredAt || transition.time);
|
|
249
|
+
if ((transition.occurredAt || transition.time) && parsed.kind === 'invalid') {
|
|
250
|
+
pushDiagnostic(diagnostics, { code: 'invalid-time', level: 'warning', message: '跳转时间无法解析。', fieldPath: `${fieldPath}.occurredAt`, transitionId: id });
|
|
251
|
+
} else if (parsed.kind === 'naive') {
|
|
252
|
+
pushDiagnostic(diagnostics, { code: 'naive-time', level: 'info', message: '跳转时间缺少时区。', fieldPath: `${fieldPath}.occurredAt`, transitionId: id });
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
runtime.edgeVisits.forEach((visit, index) => {
|
|
257
|
+
if (visit.id) {
|
|
258
|
+
if (edgeVisitIds.has(visit.id)) pushDiagnostic(diagnostics, { code: 'duplicate-edge-visit-id', level: 'error', message: '连线经过 ID 重复。', fieldPath: `edgeVisits[${index}]`, edgeId: visit.edgeId });
|
|
259
|
+
edgeVisitIds.add(visit.id);
|
|
260
|
+
}
|
|
261
|
+
if (model && visit.edgeId && !edgeIds.has(visit.edgeId)) {
|
|
262
|
+
pushDiagnostic(diagnostics, { code: 'unknown-edge', level: 'error', message: '连线经过引用了不存在的 BPMN 连线。', fieldPath: `edgeVisits[${index}].edgeId`, edgeId: visit.edgeId });
|
|
263
|
+
}
|
|
264
|
+
});
|
|
187
265
|
}
|
|
188
266
|
|
|
189
|
-
function
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
|
|
267
|
+
function resolveInvalidatedElements(transition, { activities, model, diagnostics }) {
|
|
268
|
+
const legacy = Array.isArray(transition.invalidatedActivityIds) ? transition.invalidatedActivityIds.map(String) : null;
|
|
269
|
+
const next = Array.isArray(transition.invalidatedElementIds) ? transition.invalidatedElementIds.map(String) : null;
|
|
270
|
+
const activityIds = new Set(activities.map((record) => record.id).filter(Boolean));
|
|
271
|
+
const nodeIds = new Set((model?.nodes || []).map((node) => node.id));
|
|
272
|
+
const warnIds = (ids, field) => {
|
|
273
|
+
ids.forEach((id) => {
|
|
274
|
+
if (activityIds.has(id) && !nodeIds.has(id)) {
|
|
275
|
+
pushDiagnostic(diagnostics, {
|
|
276
|
+
code: 'invalidation-work-item-id',
|
|
277
|
+
level: 'warning',
|
|
278
|
+
message: '失效字段收到工作项 ID,该字段只接受 BPMN 元素 ID。',
|
|
279
|
+
fieldPath: `transitions.${transition.id}.${field}`,
|
|
280
|
+
transitionId: transition.id,
|
|
281
|
+
activityId: id,
|
|
282
|
+
});
|
|
283
|
+
} else if (model && !nodeIds.has(id)) {
|
|
284
|
+
pushDiagnostic(diagnostics, {
|
|
285
|
+
code: 'unknown-element',
|
|
286
|
+
level: 'error',
|
|
287
|
+
message: '失效范围引用了不存在的 BPMN 元素。',
|
|
288
|
+
fieldPath: `transitions.${transition.id}.${field}`,
|
|
289
|
+
transitionId: transition.id,
|
|
290
|
+
elementId: id,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
};
|
|
295
|
+
if (legacy && next) {
|
|
296
|
+
const same = legacy.length === next.length && [...legacy].sort().every((id, index) => id === [...next].sort()[index]);
|
|
297
|
+
if (!same) {
|
|
298
|
+
pushDiagnostic(diagnostics, {
|
|
299
|
+
code: 'invalidation-field-conflict',
|
|
300
|
+
level: 'warning',
|
|
301
|
+
message: 'invalidatedElementIds 与 invalidatedActivityIds 不一致,已按元素字段生效。',
|
|
302
|
+
fieldPath: `transitions.${transition.id}.invalidatedElementIds`,
|
|
303
|
+
transitionId: transition.id,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
warnIds(next, 'invalidatedElementIds');
|
|
307
|
+
return [...next];
|
|
308
|
+
}
|
|
309
|
+
const resolved = [...(next || legacy || [])];
|
|
310
|
+
warnIds(resolved, next ? 'invalidatedElementIds' : 'invalidatedActivityIds');
|
|
311
|
+
return resolved;
|
|
193
312
|
}
|
|
194
313
|
|
|
195
314
|
function inferInvalidatedPath(model, runtime, transition) {
|
|
@@ -227,8 +346,9 @@ function inferInvalidatedPath(model, runtime, transition) {
|
|
|
227
346
|
return { activityIds: paths[0].nodeIds.slice(1), edgeIds: paths[0].edgeIds, issues: [] };
|
|
228
347
|
}
|
|
229
348
|
|
|
230
|
-
export function normalizeRuntime(snapshot = {}) {
|
|
349
|
+
export function normalizeRuntime(snapshot = {}, options = {}) {
|
|
231
350
|
snapshot = snapshot || {};
|
|
351
|
+
const diagnostics = options.diagnostics || null;
|
|
232
352
|
const activities = Array.isArray(snapshot.activities)
|
|
233
353
|
? snapshot.activities.map((record) => ({ ...record, participant: normalizeParticipant(record) || undefined }))
|
|
234
354
|
: [];
|
|
@@ -274,10 +394,13 @@ export function normalizeRuntime(snapshot = {}) {
|
|
|
274
394
|
? snapshot.edgeVisits.map((visit, index) => ({ id: visit.id || `runtime-edge-visit-${index + 1}`, status: visit.status || 'effective', ...visit }))
|
|
275
395
|
: [];
|
|
276
396
|
const orderedActions = actions
|
|
277
|
-
.map((action, index) => ({ action,
|
|
278
|
-
.sort((
|
|
397
|
+
.map((action, index) => ({ action, index }))
|
|
398
|
+
.sort((left, right) => compareRuntimeOrder(
|
|
399
|
+
{ instant: runtimeOrder(left.action), index: left.index },
|
|
400
|
+
{ instant: runtimeOrder(right.action), index: right.index },
|
|
401
|
+
))
|
|
279
402
|
.map(({ action }) => action);
|
|
280
|
-
|
|
403
|
+
const normalized = {
|
|
281
404
|
...snapshot,
|
|
282
405
|
processInstanceId: snapshot.processInstanceId || '',
|
|
283
406
|
status: snapshot.status || 'running',
|
|
@@ -287,20 +410,45 @@ export function normalizeRuntime(snapshot = {}) {
|
|
|
287
410
|
transitions,
|
|
288
411
|
edgeVisits,
|
|
289
412
|
};
|
|
413
|
+
if (diagnostics) collectIdentityDiagnostics(normalized, { model: options.model, diagnostics });
|
|
414
|
+
return normalized;
|
|
290
415
|
}
|
|
291
416
|
|
|
292
|
-
export function activityState(runtime, elementId) {
|
|
293
|
-
if (!runtime) return { status: 'idle', records: [], latestRecords: [], visits: [] };
|
|
417
|
+
export function activityState(runtime, elementId, options = {}) {
|
|
418
|
+
if (!runtime) return { status: 'idle', records: [], latestRecords: [], visits: [], effectiveVisits: [] };
|
|
294
419
|
const records = runtime.activities.filter((item) => item.elementId === elementId);
|
|
295
|
-
const visits = groupVisits(records)
|
|
296
|
-
|
|
297
|
-
|
|
420
|
+
const visits = markEffectiveVisits(groupVisits(records), {
|
|
421
|
+
elementId,
|
|
422
|
+
transitions: runtime.transitions || [],
|
|
423
|
+
diagnostics: options.diagnostics || null,
|
|
424
|
+
});
|
|
425
|
+
const effectiveVisits = visits.filter((visit) => visit.effective);
|
|
426
|
+
const latestRecords = (effectiveVisits.at(-1) || visits.at(-1))?.records || [];
|
|
427
|
+
return {
|
|
428
|
+
status: resolveEffectiveStatus(effectiveVisits.length ? effectiveVisits : visits),
|
|
429
|
+
records,
|
|
430
|
+
latestRecords,
|
|
431
|
+
visits,
|
|
432
|
+
effectiveVisits,
|
|
433
|
+
};
|
|
298
434
|
}
|
|
299
435
|
|
|
300
|
-
export function
|
|
301
|
-
const
|
|
436
|
+
export function inspectRuntime(snapshot, options = {}) {
|
|
437
|
+
const diagnostics = [];
|
|
438
|
+
const presentation = createRuntimePresentation({
|
|
439
|
+
model: options.model || { id: '', nodes: [], edges: [] },
|
|
440
|
+
runtime: snapshot,
|
|
441
|
+
appearance: options.appearance || null,
|
|
442
|
+
diagnostics,
|
|
443
|
+
});
|
|
444
|
+
return { runtime: presentation.runtime, diagnostics: presentation.diagnostics };
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
export function createRuntimePresentation({ model, runtime, appearance = null, diagnostics = null } = {}) {
|
|
448
|
+
const collected = diagnostics || [];
|
|
449
|
+
const normalized = normalizeRuntime(runtime, { diagnostics: collected, model });
|
|
302
450
|
const nodes = new Map((model?.nodes || []).map((node) => [node.id, node]));
|
|
303
|
-
const activityStates = new Map((model?.nodes || []).map((node) => [node.id, activityState(normalized, node.id)]));
|
|
451
|
+
const activityStates = new Map((model?.nodes || []).map((node) => [node.id, activityState(normalized, node.id, { diagnostics: collected })]));
|
|
304
452
|
const nodePresentations = new Map();
|
|
305
453
|
const actionPresentations = normalized.actions.map((action, index) => {
|
|
306
454
|
const assets = action.content.assets || [];
|
|
@@ -319,7 +467,7 @@ export function createRuntimePresentation({ model, runtime, appearance = null }
|
|
|
319
467
|
imageCount: images.length,
|
|
320
468
|
fileCount: files.length,
|
|
321
469
|
assetCount: images.length + files.length,
|
|
322
|
-
order: runtimeOrder(action
|
|
470
|
+
order: runtimeOrder(action) ?? index,
|
|
323
471
|
};
|
|
324
472
|
const resolvedAppearance = appearance?.resolveAction?.(baseAction);
|
|
325
473
|
return resolvedAppearance
|
|
@@ -328,20 +476,27 @@ export function createRuntimePresentation({ model, runtime, appearance = null }
|
|
|
328
476
|
});
|
|
329
477
|
const actionMap = new Map(actionPresentations.map((action) => [action.id, action]));
|
|
330
478
|
const rawTransitions = normalized.transitions
|
|
331
|
-
.map((transition, index) => ({ ...transition, order: transitionOrder(transition
|
|
332
|
-
.sort((
|
|
479
|
+
.map((transition, index) => ({ ...transition, order: transitionOrder(transition) ?? index }))
|
|
480
|
+
.sort((left, right) => compareRuntimeOrder(
|
|
481
|
+
{ instant: transitionOrder(left), index: left.order },
|
|
482
|
+
{ instant: transitionOrder(right), index: right.order },
|
|
483
|
+
));
|
|
333
484
|
const transitionPresentations = rawTransitions.map((transition) => {
|
|
334
485
|
const targetName = nodes.get(transition.targetElementId)?.name || transition.targetElementId || '';
|
|
335
486
|
const labelPrefix = transition.type === 'reject' ? '驳回至' : transition.type === 'return' ? '退回至' : transition.type === 'skip' ? '跳过' : '流转至';
|
|
336
487
|
const targetState = activityStates.get(transition.targetElementId);
|
|
337
|
-
const targetRecordsAfter = (targetState?.records || []).filter((record
|
|
488
|
+
const targetRecordsAfter = (targetState?.records || []).filter((record) => isAfter(recordOrder(record), transition.order));
|
|
338
489
|
const targetStatusAfter = resolveVisitStatus(groupVisits(targetRecordsAfter).at(-1)?.records || []);
|
|
339
490
|
const state = transition.state || (ABNORMAL_TRANSITION_TYPES.has(transition.type)
|
|
340
491
|
? (targetRecordsAfter.length && targetStatusAfter !== 'active' ? 'resolved' : 'active')
|
|
341
492
|
: 'resolved');
|
|
342
|
-
const hasExplicitInvalidation = Array.isArray(transition.invalidatedActivityIds) || Array.isArray(transition.invalidatedEdgeIds);
|
|
493
|
+
const hasExplicitInvalidation = Array.isArray(transition.invalidatedElementIds) || Array.isArray(transition.invalidatedActivityIds) || Array.isArray(transition.invalidatedEdgeIds);
|
|
343
494
|
const invalidation = hasExplicitInvalidation
|
|
344
|
-
? {
|
|
495
|
+
? {
|
|
496
|
+
activityIds: resolveInvalidatedElements(transition, { activities: normalized.activities, model, diagnostics: collected }),
|
|
497
|
+
edgeIds: [...(transition.invalidatedEdgeIds || [])],
|
|
498
|
+
issues: [],
|
|
499
|
+
}
|
|
345
500
|
: ABNORMAL_TRANSITION_TYPES.has(transition.type)
|
|
346
501
|
? inferInvalidatedPath(model, normalized, transition)
|
|
347
502
|
: { activityIds: [], edgeIds: [], issues: [] };
|
|
@@ -354,6 +509,7 @@ export function createRuntimePresentation({ model, runtime, appearance = null }
|
|
|
354
509
|
state,
|
|
355
510
|
visible: ABNORMAL_TRANSITION_TYPES.has(transition.type) && state === 'active',
|
|
356
511
|
resolvedAt: transition.resolvedAt || (state === 'resolved' ? targetRecordsAfter.map((record) => record.endTime).filter(Boolean).at(-1) : undefined),
|
|
512
|
+
invalidatedElementIds: invalidation.activityIds,
|
|
357
513
|
invalidatedActivityIds: invalidation.activityIds,
|
|
358
514
|
invalidatedEdgeIds: invalidation.edgeIds,
|
|
359
515
|
issues: invalidation.issues,
|
|
@@ -365,15 +521,38 @@ export function createRuntimePresentation({ model, runtime, appearance = null }
|
|
|
365
521
|
const invalidatedNodes = new Map();
|
|
366
522
|
const invalidatedEdges = new Map();
|
|
367
523
|
const hasActivityAfter = (elementId, transition) => (activityStates.get(elementId)?.records || [])
|
|
368
|
-
.some((record
|
|
524
|
+
.some((record) => isAfter(recordOrder(record), transition.order));
|
|
369
525
|
const hasEdgeVisitAfter = (edgeId, transition) => {
|
|
370
526
|
const edgeVisits = normalized.edgeVisits.filter((visit) => visit.edgeId === edgeId);
|
|
371
|
-
if (edgeVisits.length) return edgeVisits.some((visit
|
|
527
|
+
if (edgeVisits.length) return edgeVisits.some((visit) => visit.status !== 'superseded' && isAfter(edgeVisitOrder(visit), transition.order));
|
|
372
528
|
const edge = model?.edges?.find((item) => item.id === edgeId);
|
|
373
529
|
return edge ? hasActivityAfter(edge.target, transition) : false;
|
|
374
530
|
};
|
|
375
531
|
for (const transition of transitionPresentations.filter((item) => ABNORMAL_TRANSITION_TYPES.has(item.type))) {
|
|
376
|
-
for (const elementId of transition.invalidatedActivityIds)
|
|
532
|
+
for (const elementId of transition.invalidatedActivityIds) {
|
|
533
|
+
const state = activityStates.get(elementId);
|
|
534
|
+
const concurrent = (state?.effectiveVisits || []).length > 1;
|
|
535
|
+
if (concurrent) {
|
|
536
|
+
pushDiagnostic(collected, {
|
|
537
|
+
code: 'element-invalidation-with-concurrent-visits',
|
|
538
|
+
level: 'warning',
|
|
539
|
+
message: '元素级失效遇到多个有效 Visit,未取消无关的并发执行。',
|
|
540
|
+
fieldPath: `transitions.${transition.id}.invalidatedElementIds`,
|
|
541
|
+
transitionId: transition.id,
|
|
542
|
+
elementId,
|
|
543
|
+
});
|
|
544
|
+
const endedBefore = (state?.visits || []).filter((visit) => {
|
|
545
|
+
const ended = visit.records.map((record) => recordOrder(record)).filter((value) => value != null);
|
|
546
|
+
const last = ended.length ? Math.max(...ended) : null;
|
|
547
|
+
return last != null && last <= transition.order;
|
|
548
|
+
});
|
|
549
|
+
if (endedBefore.length && endedBefore.length < (state?.visits || []).length && !hasActivityAfter(elementId, transition)) {
|
|
550
|
+
invalidatedNodes.set(elementId, transition);
|
|
551
|
+
}
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
554
|
+
if (!hasActivityAfter(elementId, transition)) invalidatedNodes.set(elementId, transition);
|
|
555
|
+
}
|
|
377
556
|
for (const edgeId of transition.invalidatedEdgeIds) if (!hasEdgeVisitAfter(edgeId, transition)) invalidatedEdges.set(edgeId, transition);
|
|
378
557
|
}
|
|
379
558
|
|
|
@@ -384,32 +563,40 @@ export function createRuntimePresentation({ model, runtime, appearance = null }
|
|
|
384
563
|
const visitActions = actions.filter((action) => action.visitId === visit.id);
|
|
385
564
|
return { ...visit, actions: visitActions, ...summarizeApprovalActions(visitActions) };
|
|
386
565
|
});
|
|
387
|
-
const
|
|
566
|
+
const effectiveVisits = visits.filter((visit) => visit.effective);
|
|
567
|
+
const latestVisit = effectiveVisits.at(-1) || visits.at(-1) || null;
|
|
388
568
|
const latestRecords = latestVisit?.records || [];
|
|
389
|
-
const
|
|
569
|
+
const currentRecords = effectiveVisits.flatMap((visit) => visit.records);
|
|
570
|
+
const currentActions = effectiveVisits.flatMap((visit) => visit.actions);
|
|
571
|
+
const participants = uniqueParticipants(currentRecords.length ? currentRecords : latestRecords);
|
|
390
572
|
const approvalMode = latestRecords.find((item) => item.approvalMode)?.approvalMode || null;
|
|
391
573
|
const multiInstanceMode = latestRecords.find((item) => item.multiInstanceMode)?.multiInstanceMode || null;
|
|
392
574
|
const declaredTotal = latestRecords.reduce((max, item) => Math.max(max, Number(item.totalInstances) || 0), 0);
|
|
393
|
-
const total = Math.max(declaredTotal, latestRecords.length,
|
|
575
|
+
const total = Math.max(declaredTotal, latestRecords.length, uniqueParticipants(latestRecords).length);
|
|
394
576
|
const completed = latestRecords.filter((item) => item.status === 'completed').length;
|
|
395
577
|
const required = latestRecords.reduce((max, item) => Math.max(max, Number(item.requiredInstances) || 0), 0) || (approvalMode === 'any' ? 1 : total);
|
|
396
578
|
const inboundReentry = transitionPresentations.some((item) => ABNORMAL_TRANSITION_TYPES.has(item.type) && item.targetElementId === node.id);
|
|
397
579
|
const latestSourceRejection = [...transitionPresentations].reverse().find((item) => item.type === 'reject' && item.sourceElementId === node.id);
|
|
398
580
|
const rejectionSource = latestRecords.some((item) => item.outcome === 'rejected')
|
|
399
|
-
|| (latestSourceRejection && (!latestVisit || latestSourceRejection.order >=
|
|
581
|
+
|| (latestSourceRejection && (!latestVisit || (latestSourceRejection.order ?? 0) >= (recordOrder(latestRecords.at(-1) || {}) ?? 0)));
|
|
400
582
|
const skipped = !state.records.length && transitionPresentations.some((item) => item.type === 'skip' && item.targetElementId === node.id);
|
|
401
583
|
let status = skipped ? 'skipped' : state.status;
|
|
402
|
-
if (rejectionSource && status === 'completed') status = 'rejected';
|
|
584
|
+
if (rejectionSource && status === 'completed' && effectiveVisits.length <= 1) status = 'rejected';
|
|
403
585
|
const isReentry = inboundReentry && Boolean(latestVisit) && (latestVisit.round > 1 || status === 'active');
|
|
404
586
|
const superseded = invalidatedNodes.has(node.id);
|
|
405
587
|
const pathStatus = superseded || status === 'rejected' ? 'idle' : status;
|
|
406
588
|
|
|
407
589
|
let statusLabel = baseStatusLabel(status);
|
|
408
|
-
|
|
590
|
+
const activeVisitCount = effectiveVisits.filter((visit) => resolveVisitStatus(visit.records) === 'active').length;
|
|
591
|
+
if (effectiveVisits.length > 1 && activeVisitCount) {
|
|
592
|
+
statusLabel = activeVisitCount === effectiveVisits.length
|
|
593
|
+
? `处理中 ${activeVisitCount} 项执行`
|
|
594
|
+
: `处理中 ${activeVisitCount}/${effectiveVisits.length} 项执行`;
|
|
595
|
+
} else if (isReentry && status === 'active') statusLabel = '重新审批';
|
|
409
596
|
else if (status === 'active' || status === 'completed') {
|
|
410
597
|
if (approvalMode === 'all') statusLabel = `${multiInstanceMode === 'sequential' ? '顺序会签' : '会签'} ${completed}/${total || required}`;
|
|
411
598
|
else if (approvalMode === 'any') statusLabel = `或签 ${completed}/${total || required}`;
|
|
412
|
-
else if (participants.length > 1) statusLabel = `多人审批 ${completed}/${total}`;
|
|
599
|
+
else if (participants.length > 1 && effectiveVisits.length <= 1) statusLabel = `多人审批 ${completed}/${total}`;
|
|
413
600
|
}
|
|
414
601
|
|
|
415
602
|
let summary = participantSummary(participants);
|
|
@@ -424,30 +611,35 @@ export function createRuntimePresentation({ model, runtime, appearance = null }
|
|
|
424
611
|
}
|
|
425
612
|
|
|
426
613
|
const relatedTransitions = transitionPresentations.filter((item) => item.sourceElementId === node.id || item.targetElementId === node.id);
|
|
427
|
-
const
|
|
614
|
+
const currentSummary = summarizeApprovalActions(currentActions);
|
|
615
|
+
const historySummary = summarizeApprovalActions(actions);
|
|
428
616
|
nodePresentations.set(node.id, {
|
|
429
617
|
elementId: node.id, status, pathStatus, superseded, statusLabel, summary, fullSummary: participants.map((item) => item.name).join('、') || summary,
|
|
430
618
|
participants, approvalMode, multiInstanceMode, completed, total, required, round: latestVisit?.round || 0,
|
|
431
|
-
records: state.records, latestRecords, visits, transitions: relatedTransitions, actions,
|
|
619
|
+
records: state.records, latestRecords, visits, effectiveVisits, transitions: relatedTransitions, actions,
|
|
620
|
+
...currentSummary,
|
|
621
|
+
latestNonEmptyComment: historySummary.latestNonEmptyComment,
|
|
432
622
|
isReentry,
|
|
433
623
|
hasDetails: Boolean(state.records.length || actions.length || relatedTransitions.length),
|
|
434
624
|
});
|
|
435
625
|
}
|
|
436
626
|
|
|
627
|
+
const frozenDiagnostics = Object.freeze(collected.slice());
|
|
628
|
+
const emptyNode = (elementId) => ({
|
|
629
|
+
elementId, status: 'idle', pathStatus: 'idle', superseded: false, statusLabel: '未到达', summary: '', fullSummary: '', participants: [], approvalMode: null,
|
|
630
|
+
multiInstanceMode: null, completed: 0, total: 0, required: 0, round: 0, records: [], latestRecords: [], visits: [], effectiveVisits: [], transitions: [], actions: [], latestAction: null,
|
|
631
|
+
latestNonEmptyComment: null, actionText: '', actionSummary: '', imageCount: 0, fileCount: 0, assetCount: 0, isReentry: false, hasDetails: false,
|
|
632
|
+
});
|
|
633
|
+
|
|
437
634
|
return {
|
|
438
635
|
runtime: normalized,
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
elementId, status: 'idle', pathStatus: 'idle', superseded: false, statusLabel: '未到达', summary: '', fullSummary: '', participants: [], approvalMode: null,
|
|
442
|
-
multiInstanceMode: null, completed: 0, total: 0, required: 0, round: 0, records: [], latestRecords: [], visits: [], transitions: [], actions: [], latestAction: null,
|
|
443
|
-
actionText: '', actionSummary: '', imageCount: 0, fileCount: 0, assetCount: 0, isReentry: false, hasDetails: false,
|
|
444
|
-
};
|
|
445
|
-
},
|
|
636
|
+
diagnostics: frozenDiagnostics,
|
|
637
|
+
getNode(elementId) { return nodePresentations.get(elementId) || emptyNode(elementId); },
|
|
446
638
|
getEdge(edgeId) {
|
|
447
639
|
const edgeVisits = normalized.edgeVisits.filter((visit) => visit.edgeId === edgeId);
|
|
448
640
|
const latestVisit = edgeVisits
|
|
449
|
-
.map((visit, index) => ({ visit,
|
|
450
|
-
.sort((
|
|
641
|
+
.map((visit, index) => ({ visit, index, instant: edgeVisitOrder(visit) }))
|
|
642
|
+
.sort((left, right) => compareRuntimeOrder(left, right))
|
|
451
643
|
.at(-1)?.visit;
|
|
452
644
|
const visited = normalized.visitedEdges.includes(edgeId) || edgeVisits.length > 0;
|
|
453
645
|
const superseded = invalidatedEdges.has(edgeId) || latestVisit?.status === 'superseded';
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const OFFSET_PATTERN = /(?:Z|[+-]\d{2}:?\d{2})$/i;
|
|
2
|
+
|
|
3
|
+
export function parseRuntimeInstant(value) {
|
|
4
|
+
if (value == null || value === '') return { instant: null, raw: '', kind: 'missing' };
|
|
5
|
+
const raw = String(value);
|
|
6
|
+
const trimmed = raw.trim();
|
|
7
|
+
if (!trimmed) return { instant: null, raw, kind: 'missing' };
|
|
8
|
+
const hasOffset = OFFSET_PATTERN.test(trimmed);
|
|
9
|
+
const normalized = !trimmed.includes('T') && trimmed.includes(' ') ? trimmed.replace(' ', 'T') : trimmed;
|
|
10
|
+
const instant = Date.parse(normalized);
|
|
11
|
+
if (!Number.isFinite(instant)) return { instant: null, raw, kind: 'invalid' };
|
|
12
|
+
if (hasOffset) return { instant, raw, kind: /Z$/i.test(trimmed) ? 'z' : 'offset' };
|
|
13
|
+
return { instant, raw, kind: 'naive' };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function formatRuntimeInstant(value) {
|
|
17
|
+
const parsed = value && typeof value === 'object' && 'raw' in value ? value : parseRuntimeInstant(value);
|
|
18
|
+
if (!parsed?.raw) return '';
|
|
19
|
+
return String(parsed.raw).trim().replace('T', ' ');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function compareRuntimeOrder(left, right) {
|
|
23
|
+
const a = normalizeOrderKey(left);
|
|
24
|
+
const b = normalizeOrderKey(right);
|
|
25
|
+
if (a.instant != null && b.instant != null && a.instant !== b.instant) return a.instant - b.instant;
|
|
26
|
+
if (a.instant != null && b.instant == null) return -1;
|
|
27
|
+
if (a.instant == null && b.instant != null) return 1;
|
|
28
|
+
return (a.index || 0) - (b.index || 0);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function runtimeInstant(value) {
|
|
32
|
+
return parseRuntimeInstant(value).instant;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function normalizeOrderKey(value) {
|
|
36
|
+
if (value && typeof value === 'object' && ('instant' in value || 'index' in value || 'kind' in value)) {
|
|
37
|
+
return { instant: value.instant ?? null, index: Number.isFinite(value.index) ? value.index : 0, kind: value.kind };
|
|
38
|
+
}
|
|
39
|
+
const parsed = parseRuntimeInstant(value);
|
|
40
|
+
return { instant: parsed.instant, index: 0, kind: parsed.kind };
|
|
41
|
+
}
|