@xema/omni-protocol 0.1.76 → 0.1.77
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/testing.d.ts +9 -0
- package/dist/testing.js +48 -20
- package/guide.md +9 -2
- package/package.json +1 -1
package/dist/testing.d.ts
CHANGED
|
@@ -170,6 +170,15 @@ export declare class TaskStream {
|
|
|
170
170
|
/** What a restated record lost of the one read before it: nothing, or the entries by step and instant. */
|
|
171
171
|
private static lost;
|
|
172
172
|
private static stated;
|
|
173
|
+
/**
|
|
174
|
+
* Takes a resync snapshot -- a `snapshot` event, or the read a reloaded client makes -- as the
|
|
175
|
+
* state now, and first holds it to what the stream knew. A snapshot replaces state; it does not
|
|
176
|
+
* get to forget it. A task it carries that the stream already held may not go backwards, lose an
|
|
177
|
+
* entry of its record, return to undetermined terms, or be at work without the audio the stream
|
|
178
|
+
* held up: media ends on task-media-ended and the call moves on, so a task still in-progress or
|
|
179
|
+
* paused without its media is a snapshot that lost state, not a call that ended.
|
|
180
|
+
*/
|
|
181
|
+
resync(snapshot: unknown, at: string): ProtocolViolation[];
|
|
173
182
|
/** Replaces what is known with a snapshot's tasks, as a snapshot replaces Omni's state. */
|
|
174
183
|
seed(snapshot: unknown): void;
|
|
175
184
|
/** Applies one envelope and returns what it may not say given what came before. */
|
package/dist/testing.js
CHANGED
|
@@ -1047,6 +1047,49 @@ export class TaskStream {
|
|
|
1047
1047
|
wrapAllowance: isRecord(task) && typeof task.wrapAllowance === "number" ? task.wrapAllowance : undefined,
|
|
1048
1048
|
phase: String(isRecord(task) ? task.phase : undefined), media, source: String(isRecord(task) ? task.capabilitySource : undefined), stages, record: TaskStream.record(task), allocation: String(isRecord(task) ? task.allocationId : undefined) };
|
|
1049
1049
|
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Takes a resync snapshot -- a `snapshot` event, or the read a reloaded client makes -- as the
|
|
1052
|
+
* state now, and first holds it to what the stream knew. A snapshot replaces state; it does not
|
|
1053
|
+
* get to forget it. A task it carries that the stream already held may not go backwards, lose an
|
|
1054
|
+
* entry of its record, return to undetermined terms, or be at work without the audio the stream
|
|
1055
|
+
* held up: media ends on task-media-ended and the call moves on, so a task still in-progress or
|
|
1056
|
+
* paused without its media is a snapshot that lost state, not a call that ended.
|
|
1057
|
+
*/
|
|
1058
|
+
resync(snapshot, at) {
|
|
1059
|
+
const found = [];
|
|
1060
|
+
const refuse = (rule, where, message) => found.push({ rule, path: where, message });
|
|
1061
|
+
ruleEvaluated("stream.snapshot.capabilitySource", "stream.snapshot.handlingHistory", "stream.snapshot.phase", "stream.snapshot.media");
|
|
1062
|
+
if (isRecord(snapshot) && Array.isArray(snapshot.tasks)) {
|
|
1063
|
+
snapshot.tasks.forEach((task, index) => {
|
|
1064
|
+
if (!isRecord(task) || typeof task.id !== "string")
|
|
1065
|
+
return;
|
|
1066
|
+
const was = this.tasks.get(task.id);
|
|
1067
|
+
if (was === undefined || was.allocation !== String(task.allocationId))
|
|
1068
|
+
return;
|
|
1069
|
+
if ((was.source === "queue" || was.source === "ungoverned") && task.capabilitySource === "undetermined") {
|
|
1070
|
+
refuse("stream.snapshot.capabilitySource", `${at}.tasks[${index}].capabilitySource`, `${task.id} was published under ${was.source} terms and the snapshot says undetermined: terms once read stay read`);
|
|
1071
|
+
}
|
|
1072
|
+
// A record once read is not unread: a resync restates it whole, or with more, never with less.
|
|
1073
|
+
const lost = TaskStream.lost(was.record, TaskStream.record(task));
|
|
1074
|
+
if (lost.length > 0) {
|
|
1075
|
+
refuse("stream.snapshot.handlingHistory", `${at}.tasks[${index}].handlingHistory`, `${task.id}'s record lost ${lost.join(", ")} on the snapshot: an entry read by the host stays in the record until the task ends`);
|
|
1076
|
+
}
|
|
1077
|
+
const to = String(task.phase);
|
|
1078
|
+
const reachable = REACHABLE_PHASES[was.phase];
|
|
1079
|
+
if (reachable !== undefined && TASK_PHASES_ORDERED.includes(to) && !reachable.has(to)) {
|
|
1080
|
+
const connectingBack = was.phase === "completing" && TaskStream.partyDialled(task) && (to === "in-progress" || to === "paused");
|
|
1081
|
+
if (!connectingBack) {
|
|
1082
|
+
refuse("stream.snapshot.phase", `${at}.tasks[${index}].phase`, `${task.id} was ${was.phase} and the snapshot says ${to}: a task does not go backwards, on an update or on a resync`);
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
if (was.media === "started" && (to === "in-progress" || to === "paused") && task.media !== "started") {
|
|
1086
|
+
refuse("stream.snapshot.media", `${at}.tasks[${index}].media`, `${task.id}'s audio was up and the snapshot carries it ${to} without it: media ends on task-media-ended and the call moves on, so a snapshot that forgets the audio lost state`);
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
this.seed(snapshot);
|
|
1091
|
+
return found;
|
|
1092
|
+
}
|
|
1050
1093
|
/** Replaces what is known with a snapshot's tasks, as a snapshot replaces Omni's state. */
|
|
1051
1094
|
seed(snapshot) {
|
|
1052
1095
|
this.tasks.clear();
|
|
@@ -1073,24 +1116,7 @@ export class TaskStream {
|
|
|
1073
1116
|
const known = id === undefined ? undefined : this.tasks.get(id);
|
|
1074
1117
|
switch (event.type) {
|
|
1075
1118
|
case "snapshot":
|
|
1076
|
-
|
|
1077
|
-
// A snapshot replaces what is known, and still may not say a task lost terms it had read.
|
|
1078
|
-
if (isRecord(event.snapshot) && Array.isArray(event.snapshot.tasks)) {
|
|
1079
|
-
event.snapshot.tasks.forEach((task, index) => {
|
|
1080
|
-
if (!isRecord(task) || typeof task.id !== "string")
|
|
1081
|
-
return;
|
|
1082
|
-
const was = this.tasks.get(task.id);
|
|
1083
|
-
if (was !== undefined && (was.source === "queue" || was.source === "ungoverned") && task.capabilitySource === "undetermined") {
|
|
1084
|
-
refuse("stream.snapshot.capabilitySource", `${at}.snapshot.tasks[${index}].capabilitySource`, `${task.id} was published under ${was.source} terms and the snapshot says undetermined: terms once read stay read`);
|
|
1085
|
-
}
|
|
1086
|
-
// A record once read is not unread: a resync restates it whole, or with more, never with less.
|
|
1087
|
-
const lost = TaskStream.lost(was?.record, TaskStream.record(task));
|
|
1088
|
-
if (lost.length > 0) {
|
|
1089
|
-
refuse("stream.snapshot.handlingHistory", `${at}.snapshot.tasks[${index}].handlingHistory`, `${task.id}'s record lost ${lost.join(", ")} on the snapshot: an entry read by the host stays in the record until the task ends`);
|
|
1090
|
-
}
|
|
1091
|
-
});
|
|
1092
|
-
}
|
|
1093
|
-
this.seed(event.snapshot);
|
|
1119
|
+
found.push(...this.resync(event.snapshot, `${at}.snapshot`));
|
|
1094
1120
|
break;
|
|
1095
1121
|
case "task-offered": {
|
|
1096
1122
|
ruleEvaluated("stream.taskOffered.duplicate", "stream.taskOffered.allocation");
|
|
@@ -1346,9 +1372,11 @@ async function driveOneCall(drive) {
|
|
|
1346
1372
|
drive.handOver.up(session, second);
|
|
1347
1373
|
drive.connection = second;
|
|
1348
1374
|
const snapshot = await second.snapshot();
|
|
1349
|
-
// The second adapter's snapshot is held to everything a first one is, and the streams take it
|
|
1375
|
+
// The second adapter's snapshot is held to everything a first one is, and the streams take it
|
|
1376
|
+
// as the state now the way they take any resync: held to what they knew, then replaced. A reload
|
|
1377
|
+
// is a place the stream's rules keep working, not one where they all stop.
|
|
1350
1378
|
found.push(...validateSnapshot(snapshot, again.manifest, "drive.reload.snapshot", drive.reader()));
|
|
1351
|
-
drive.streams.stream.
|
|
1379
|
+
found.push(...drive.streams.stream.resync(snapshot, "drive.reload.snapshot"));
|
|
1352
1380
|
drive.streams.breaks.seed(snapshot);
|
|
1353
1381
|
const carried = isRecord(snapshot) && Array.isArray(snapshot.tasks)
|
|
1354
1382
|
? snapshot.tasks.find(task => isRecord(task) && task.id === taskId) : undefined;
|
package/guide.md
CHANGED
|
@@ -2888,7 +2888,10 @@ last statement stands and the failure is a `diagnostic`, and a task that was pub
|
|
|
2888
2888
|
`queue` or `ungoverned` never returns to `undetermined`, on an update (`stream.taskUpdated.capabilitySource`)
|
|
2889
2889
|
or on a resync snapshot (`stream.snapshot.capabilitySource`); a record once read never loses an
|
|
2890
2890
|
entry the same two ways (`stream.taskUpdated.handlingHistory`, `stream.snapshot.handlingHistory`);
|
|
2891
|
-
|
|
2891
|
+
a snapshot carrying a task still at work does not forget the audio the stream held up, since media
|
|
2892
|
+
ends on `task-media-ended` and the call moves on (`stream.snapshot.media`); and a task does not go
|
|
2893
|
+
backwards, on an update or on a resync (`stream.taskUpdated.phase`, `stream.snapshot.phase`): the
|
|
2894
|
+
stream sees publications, not
|
|
2892
2895
|
transitions, and a task may pass through a phase between two, so `pending` to `in-progress` stands
|
|
2893
2896
|
with `confirmed` between them, and what is refused is a phase unreachable from the last one read by
|
|
2894
2897
|
the transition table -- back to `pending`, back to `confirmed` or `preview` once work began, out of
|
|
@@ -4781,7 +4784,11 @@ built and connected for the same login with the same context and the same `store
|
|
|
4781
4784
|
must carry the task with that leg and the host's word, and the run goes on with the second as its
|
|
4782
4785
|
connection to the end. A platform that holds the record hands it back, and an adapter that composed
|
|
4783
4786
|
the record in memory has nothing and is named (`drive.reload.snapshot`, `drive.reload.history`,
|
|
4784
|
-
`.rejected`). The second adapter is held to what the
|
|
4787
|
+
`.rejected`). The second adapter's snapshot is taken as any resync is: held to what the stream knew
|
|
4788
|
+
before it replaces it, so a phase gone backwards, a record that shrank or audio forgotten on a task
|
|
4789
|
+
still at work is named by the stream's own rules (`stream.snapshot.phase`, `.handlingHistory`,
|
|
4790
|
+
`.media`) and a reload is a place those rules keep working, not one where they stop. The second
|
|
4791
|
+
adapter is held to what the first was: the same provider
|
|
4785
4792
|
(`drive.reload.manifest`), a snapshot that stands as any snapshot must, and a record that lost none
|
|
4786
4793
|
of the entries the first had published -- a record once read is not unread across a reload either.
|
|
4787
4794
|
A reconnect on the same adapter object would prove nothing, since an in-process adapter's memory
|