@milaboratories/pl-client 3.17.7 → 3.18.0
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/core/client.cjs +5 -3
- package/dist/core/client.cjs.map +1 -1
- package/dist/core/client.d.ts +5 -2
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +6 -4
- package/dist/core/client.js.map +1 -1
- package/dist/core/final.cjs +203 -70
- package/dist/core/final.cjs.map +1 -1
- package/dist/core/final.d.ts +15 -13
- package/dist/core/final.d.ts.map +1 -1
- package/dist/core/final.js +199 -69
- package/dist/core/final.js.map +1 -1
- package/dist/core/finality.cjs +84 -0
- package/dist/core/finality.cjs.map +1 -0
- package/dist/core/finality.d.ts +81 -0
- package/dist/core/finality.d.ts.map +1 -0
- package/dist/core/finality.js +82 -0
- package/dist/core/finality.js.map +1 -0
- package/dist/core/transaction.cjs +4 -4
- package/dist/core/transaction.cjs.map +1 -1
- package/dist/core/transaction.d.ts +7 -3
- package/dist/core/transaction.d.ts.map +1 -1
- package/dist/core/transaction.js +4 -4
- package/dist/core/transaction.js.map +1 -1
- package/dist/index.cjs +7 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/package.json +4 -4
- package/src/core/client.test.ts +38 -0
- package/src/core/client.ts +6 -4
- package/src/core/final.test.ts +103 -1
- package/src/core/final.ts +264 -123
- package/src/core/finality.test.ts +298 -0
- package/src/core/finality.ts +162 -0
- package/src/core/transaction.ts +3 -2
- package/src/index.ts +1 -0
package/src/core/final.test.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { expect, test } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
CacheFinality,
|
|
4
|
+
DefaultFinalResourceDataPredicate,
|
|
5
|
+
StrictFinality,
|
|
6
|
+
TreeFinality,
|
|
7
|
+
} from "./final";
|
|
3
8
|
import type { FieldData, ResourceData } from "./types";
|
|
4
9
|
import { createSignedResourceId, NullSignedResourceId } from "./types";
|
|
5
10
|
|
|
@@ -71,3 +76,100 @@ test("a StreamManager with both fields still empty is not final", () => {
|
|
|
71
76
|
),
|
|
72
77
|
).toBe(false);
|
|
73
78
|
});
|
|
79
|
+
|
|
80
|
+
function resource(typeName: string, patch: Partial<ResourceData> = {}): ResourceData {
|
|
81
|
+
return streamManager({ type: { name: typeName, version: "1" }, ...patch });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const notReady: Partial<ResourceData> = {
|
|
85
|
+
resourceReady: false,
|
|
86
|
+
inputsLocked: false,
|
|
87
|
+
outputsLocked: false,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const filledOutput = { fields: [field("out", A)] };
|
|
91
|
+
const unfilledOutput = {
|
|
92
|
+
fields: [
|
|
93
|
+
{
|
|
94
|
+
...field("out", A),
|
|
95
|
+
value: NullSignedResourceId,
|
|
96
|
+
status: "Empty" as const,
|
|
97
|
+
valueIsFinal: false,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
test("a ready resolver with locked outputs is final only once every supplied field is settled", () => {
|
|
103
|
+
for (const name of ["BResolveSingle", "BResolveChoice"]) {
|
|
104
|
+
expect(DefaultFinalResourceDataPredicate(resource(name, filledOutput))).toBe(true);
|
|
105
|
+
expect(DefaultFinalResourceDataPredicate(resource(name, unfilledOutput))).toBe(false);
|
|
106
|
+
expect(
|
|
107
|
+
DefaultFinalResourceDataPredicate(resource(name, { ...notReady, ...filledOutput })),
|
|
108
|
+
).toBe(false);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("a ready blob copy with locked outputs is final only once every supplied field is settled", () => {
|
|
113
|
+
expect(DefaultFinalResourceDataPredicate(resource("BlobCopy/aToB", filledOutput))).toBe(true);
|
|
114
|
+
expect(DefaultFinalResourceDataPredicate(resource("BlobCopy/aToB", unfilledOutput))).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("BResolveSingleNoResult is final at ready", () => {
|
|
118
|
+
expect(DefaultFinalResourceDataPredicate(resource("BResolveSingleNoResult"))).toBe(true);
|
|
119
|
+
expect(DefaultFinalResourceDataPredicate(resource("BResolveSingleNoResult", notReady))).toBe(
|
|
120
|
+
false,
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("LSProvider is never final", () => {
|
|
125
|
+
expect(DefaultFinalResourceDataPredicate(resource("LSProvider"))).toBe(false);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("frontend archives and JSON scalar and error-trace snapshots are always final", () => {
|
|
129
|
+
for (const name of ["Frontend/FromLocalTgz", "json/bool", "json/null", "json/errorTrace"])
|
|
130
|
+
expect(StrictFinality.isFinal(resource(name, notReady))).toBe(true);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("the strict layer keeps blobs, transfer handles, working directories and stream managers non-final", () => {
|
|
134
|
+
for (const name of ["Blob", "Blob/fs", "BlobIndex/fs", "BlobUpload/fs", "BlobCopy/aToB"])
|
|
135
|
+
expect(StrictFinality.isFinal(resource(name, filledOutput))).toBe(false);
|
|
136
|
+
expect(StrictFinality.isFinal(resource("WorkingDirectory", notReady))).toBe(false);
|
|
137
|
+
expect(StrictFinality.isFinal(streamManager({ error: A, fields: [] }))).toBe(false);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("the cache adds the types whose only later writes are KV", () => {
|
|
141
|
+
for (const name of ["Blob/fs", "BlobIndex/fs", "BlobUpload/fs", "BlobCopy/aToB"])
|
|
142
|
+
expect(CacheFinality.isFinal(resource(name, filledOutput))).toBe(true);
|
|
143
|
+
expect(CacheFinality.isFinal(resource("WorkingDirectory", notReady))).toBe(true);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("the cache keeps a Blob and a StreamManager out: their fields change later", () => {
|
|
147
|
+
expect(CacheFinality.isFinal(resource("Blob", notReady))).toBe(false);
|
|
148
|
+
expect(CacheFinality.isFinal(streamManager({ error: A, fields: [] }))).toBe(false);
|
|
149
|
+
expect(TreeFinality.isFinal(resource("Blob", notReady))).toBe(true);
|
|
150
|
+
expect(TreeFinality.isFinal(streamManager({ error: A, fields: [] }))).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("each layer calls final everything its parent does", () => {
|
|
154
|
+
const states: Partial<ResourceData>[] = [
|
|
155
|
+
{},
|
|
156
|
+
notReady,
|
|
157
|
+
filledOutput,
|
|
158
|
+
unfilledOutput,
|
|
159
|
+
{ error: A },
|
|
160
|
+
{ ...notReady, originalResourceId: B },
|
|
161
|
+
{ fields: undefined },
|
|
162
|
+
];
|
|
163
|
+
const typeNames = new Set<string>();
|
|
164
|
+
for (const e of TreeFinality.entries)
|
|
165
|
+
typeNames.add("name" in e.match ? e.match.name : e.match.prefix + "x");
|
|
166
|
+
for (const name of typeNames)
|
|
167
|
+
for (const state of states) {
|
|
168
|
+
const r = resource(name, state);
|
|
169
|
+
const strict = StrictFinality.isFinal(r);
|
|
170
|
+
const cache = CacheFinality.isFinal(r);
|
|
171
|
+
const tree = TreeFinality.isFinal(r);
|
|
172
|
+
expect(!strict || cache, `${name} ${JSON.stringify(state)}: strict ⊆ cache`).toBe(true);
|
|
173
|
+
expect(!cache || tree, `${name} ${JSON.stringify(state)}: cache ⊆ tree`).toBe(true);
|
|
174
|
+
}
|
|
175
|
+
});
|
package/src/core/final.ts
CHANGED
|
@@ -1,132 +1,273 @@
|
|
|
1
|
-
import type { Optional } from "utility-types";
|
|
2
|
-
import type { BasicResourceData, ResourceData } from "./types";
|
|
3
|
-
import { isNotNullSignedResourceId, isNullSignedResourceId } from "./types";
|
|
4
1
|
import { ResourceTypeName, ResourceTypePrefix } from "@milaboratories/pl-model-common";
|
|
2
|
+
import type { FinalityEntry, FinalityStopRule, FinalResourceDataPredicate } from "./finality";
|
|
3
|
+
import { FinalityTable, readyOrDuplicateOrError } from "./finality";
|
|
4
|
+
import { isNotNullSignedResourceId, isNullSignedResourceId } from "./types";
|
|
5
5
|
export { ResourceTypeName, ResourceTypePrefix };
|
|
6
|
+
export type { FinalResourceDataPredicate } from "./finality";
|
|
7
|
+
export { readyOrDuplicateOrError } from "./finality";
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
*
|
|
13
|
-
* If resource data contain information about fields, if should be taken into account, fields are undefined,
|
|
14
|
-
* "final" state should be calculated for "basic" part of resource data only.
|
|
15
|
-
*/
|
|
16
|
-
export type FinalResourceDataPredicate = (
|
|
17
|
-
resourceData: Optional<ResourceData, "fields">,
|
|
18
|
-
) => boolean;
|
|
9
|
+
// The finality tables, one layer per consumer. Every layer accepts two backend writes after
|
|
10
|
+
// final: a data-loss error cascade, lifting the error of a stored blob found lost or corrupt to
|
|
11
|
+
// the resources built from it, and the first duplicate's `hasOriginalListeners` flag on its
|
|
12
|
+
// original, backend-internal and never on the wire. An entry's `why` names any other write
|
|
13
|
+
// it accepts. A layer adds only the types whose later writes its consumer does not observe.
|
|
19
14
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
// Where the backend's filter stops earlier than the predicate. An early stop is safe: the
|
|
16
|
+
// backend sends a body-less stop marker, and the streaming loader fetches every stopped resource
|
|
17
|
+
// it does not hold as final again without stop rules (field filtering and pruning still apply),
|
|
18
|
+
// so the predicate decides on the fetched body.
|
|
19
|
+
const earlyOnFieldErrors: FinalityStopRule = {
|
|
20
|
+
approx: "readyOrDuplicateOrError",
|
|
21
|
+
reason:
|
|
22
|
+
"HAS_ERRORS is set by an error on any field, the predicate reads the resource error only; " +
|
|
23
|
+
"an early stop is followed up with a plain fetch",
|
|
24
|
+
};
|
|
25
|
+
const earlyOnFieldErrorsWithOutputs: FinalityStopRule = {
|
|
26
|
+
approx: "readyAndAllOutputsFilled",
|
|
27
|
+
reason:
|
|
28
|
+
"HAS_ERRORS is set by an error on any field, the predicate reads the resource error only; " +
|
|
29
|
+
"ALL_OUTPUTS_FINAL, like the predicate, checks every retained field; an early stop is " +
|
|
30
|
+
"followed up with a plain fetch",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const value = (name: string): FinalityEntry => ({
|
|
34
|
+
match: { name },
|
|
35
|
+
rule: "always",
|
|
36
|
+
why: "a value, written once at creation",
|
|
37
|
+
});
|
|
27
38
|
|
|
28
|
-
|
|
39
|
+
const settledAtReady = (name: string, why: string): FinalityEntry => ({
|
|
40
|
+
match: { name },
|
|
41
|
+
rule: "readyOrDuplicateOrError",
|
|
42
|
+
why,
|
|
43
|
+
stopRule: earlyOnFieldErrors,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const never = (match: FinalityEntry["match"], why: string): FinalityEntry => ({
|
|
47
|
+
match,
|
|
48
|
+
rule: "never",
|
|
49
|
+
why,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/** The base layer: nothing observable (state, fields, KV) changes after the rule holds, whatever
|
|
53
|
+
* the consumer prunes. Exceptions: the data-loss error cascade, the `hasOriginalListeners` flag,
|
|
54
|
+
* and the writes its entries name. */
|
|
55
|
+
export const StrictFinality: FinalityTable = FinalityTable.of("strict", [
|
|
56
|
+
value(ResourceTypeName.JsonObject),
|
|
57
|
+
value(ResourceTypeName.JsonGzObject),
|
|
58
|
+
value(ResourceTypeName.JsonString),
|
|
59
|
+
value(ResourceTypeName.JsonArray),
|
|
60
|
+
value(ResourceTypeName.JsonNumber),
|
|
61
|
+
value(ResourceTypeName.JsonBool),
|
|
62
|
+
value(ResourceTypeName.JsonNull),
|
|
63
|
+
{
|
|
64
|
+
match: { name: ResourceTypeName.JsonErrorTrace },
|
|
65
|
+
rule: "always",
|
|
66
|
+
why: "an error-trace snapshot, populated and locked in its creating transaction",
|
|
67
|
+
},
|
|
68
|
+
value(ResourceTypeName.BContextEnd),
|
|
69
|
+
value(ResourceTypeName.FrontendFromUrl),
|
|
70
|
+
value(ResourceTypeName.FrontendFromFolder),
|
|
71
|
+
value(ResourceTypeName.FrontendFromLocalTgz),
|
|
72
|
+
value(ResourceTypeName.BObjectSpec),
|
|
73
|
+
value(ResourceTypeName.Null),
|
|
74
|
+
value(ResourceTypeName.Binary),
|
|
75
|
+
{
|
|
76
|
+
match: { name: ResourceTypeName.JsonResourceError },
|
|
77
|
+
rule: { custom: (r) => r.type.version === "1" },
|
|
78
|
+
why: "version 1 is a value, written once at creation",
|
|
79
|
+
stopRule: { approx: "always", reason: "a stop rule cannot see the type version" },
|
|
80
|
+
},
|
|
81
|
+
{ match: { prefix: ResourceTypePrefix.LS }, rule: "always", why: "no write after creation" },
|
|
82
|
+
{
|
|
83
|
+
match: { prefix: ResourceTypePrefix.WorkingDirectory },
|
|
84
|
+
rule: "always",
|
|
85
|
+
why: "no write after creation",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
match: { prefix: ResourceTypePrefix.StorageSpaceAllocation },
|
|
89
|
+
rule: "always",
|
|
90
|
+
why: "no write after creation",
|
|
91
|
+
},
|
|
92
|
+
...[
|
|
93
|
+
ResourceTypeName.StdMap,
|
|
94
|
+
ResourceTypeName.StdMapSlash,
|
|
95
|
+
ResourceTypeName.EphStdMap,
|
|
96
|
+
ResourceTypeName.PFrame,
|
|
97
|
+
ResourceTypeName.ParquetChunk,
|
|
98
|
+
ResourceTypeName.BContext,
|
|
99
|
+
ResourceTypeName.BinaryMap,
|
|
100
|
+
ResourceTypeName.BinaryValue,
|
|
101
|
+
ResourceTypeName.BlobMap,
|
|
102
|
+
ResourceTypeName.BResolveSingleNoResult,
|
|
103
|
+
ResourceTypeName.BQueryResult,
|
|
104
|
+
ResourceTypeName.TengoLib,
|
|
105
|
+
].map((name) =>
|
|
106
|
+
settledAtReady(name, "its inputs are final at ready, and no controller fills it afterwards"),
|
|
107
|
+
),
|
|
108
|
+
...[
|
|
109
|
+
ResourceTypeName.TengoTemplate,
|
|
110
|
+
ResourceTypeName.SoftwareInfo,
|
|
111
|
+
ResourceTypeName.BlockPackCustom,
|
|
112
|
+
].map((name) =>
|
|
113
|
+
settledAtReady(
|
|
114
|
+
name,
|
|
115
|
+
"its only later write is the `hasOriginalListeners` flag its first duplicate sets",
|
|
116
|
+
),
|
|
117
|
+
),
|
|
118
|
+
settledAtReady(
|
|
119
|
+
ResourceTypeName.Dummy,
|
|
120
|
+
"its only later write is a data-loss error from the Blob whose incarnation field holds it",
|
|
121
|
+
),
|
|
122
|
+
{
|
|
123
|
+
match: { prefix: ResourceTypePrefix.PColumnData },
|
|
124
|
+
rule: "readyOrDuplicateOrError",
|
|
125
|
+
why: "its inputs are final at ready, and no controller fills it afterwards",
|
|
126
|
+
stopRule: earlyOnFieldErrors,
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
match: { prefix: ResourceTypePrefix.StreamWorkdir },
|
|
130
|
+
rule: "readyOrDuplicateOrError",
|
|
131
|
+
why: "no write after ready",
|
|
132
|
+
stopRule: {
|
|
133
|
+
approx: "readyOrDuplicateOrError",
|
|
134
|
+
reason:
|
|
135
|
+
"HAS_ERRORS is set by an error on any field; on ResourceTree walks the project field " +
|
|
136
|
+
"filter also removes every field, `resourceError` included, before decoding, so the " +
|
|
137
|
+
"predicate cannot see an error the filter stops on; an early stop is followed up with " +
|
|
138
|
+
"a plain fetch",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
match: { name: ResourceTypeName.BResolveSingle },
|
|
143
|
+
rule: "readyAndAllOutputsFilled",
|
|
144
|
+
why:
|
|
145
|
+
"outputs are locked at creation and assigned after ready, never overwritten; one " +
|
|
146
|
+
"exception: a later context with more than one match sets an error on a resolver that " +
|
|
147
|
+
"already succeeded, and a reader holding it final misses that error",
|
|
148
|
+
stopRule: earlyOnFieldErrorsWithOutputs,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
match: { name: ResourceTypeName.BResolveChoice },
|
|
152
|
+
rule: "readyAndAllOutputsFilled",
|
|
153
|
+
why:
|
|
154
|
+
"outputs are locked at creation and point at result maps that are completed and locked " +
|
|
155
|
+
"as resolution finishes",
|
|
156
|
+
stopRule: earlyOnFieldErrorsWithOutputs,
|
|
157
|
+
},
|
|
158
|
+
never({ name: ResourceTypeName.UserProject }, "blocks and fields come and go over its life"),
|
|
159
|
+
never({ name: ResourceTypeName.Projects }, "projects are added and removed"),
|
|
160
|
+
never({ name: ResourceTypeName.ClientRoot }, "its fields are added and removed"),
|
|
161
|
+
...[
|
|
162
|
+
ResourceTypeName.SharingOutbox,
|
|
163
|
+
ResourceTypeName.SharingState,
|
|
164
|
+
ResourceTypeName.SharedEnvelope,
|
|
165
|
+
].map((name) => never({ name }, "gains and loses dynamic child fields over its life")),
|
|
166
|
+
never(
|
|
167
|
+
{ name: ResourceTypeName.LSProvider },
|
|
168
|
+
"its storage fields are rewritten on a controller restart with a changed storage config",
|
|
169
|
+
),
|
|
170
|
+
never({ name: ResourceTypeName.Blob }, "its incarnation field is attached or replaced later"),
|
|
171
|
+
never({ name: ResourceTypeName.StreamManager }, "`stream` is reset after an error"),
|
|
172
|
+
never({ prefix: ResourceTypePrefix.Blob }, "its `ctl/file/blobInfo` KV is re-pointed later"),
|
|
173
|
+
never(
|
|
174
|
+
{ prefix: ResourceTypePrefix.BlobIndex },
|
|
175
|
+
"a controller restart writes its `ctl/ctlsdk/bootstrapDone` KV",
|
|
176
|
+
),
|
|
177
|
+
never(
|
|
178
|
+
{ prefix: ResourceTypePrefix.BlobUpload },
|
|
179
|
+
"the Reset RPC deletes its `ctl/file/storage/uploadState` KV",
|
|
180
|
+
),
|
|
181
|
+
never(
|
|
182
|
+
{ prefix: ResourceTypePrefix.BlobCopy },
|
|
183
|
+
"a controller bootstrap records its `ctl/ctlsdk/bootstrapDone` KV in a separate transaction, which may run after the copy settled",
|
|
184
|
+
),
|
|
185
|
+
never(
|
|
186
|
+
{ name: ResourceTypeName.WorkingDirectory },
|
|
187
|
+
"lock acquisition can write its `internal/locks/lockedBy` KV after creation",
|
|
188
|
+
),
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
/** What the transaction resource cache may keep while an entry stays in its LRU. The cache holds
|
|
192
|
+
* state and fields, never KV, so it adds the types whose only later writes are KV. */
|
|
193
|
+
export const CacheFinality: FinalityTable = StrictFinality.extend("cache", [
|
|
194
|
+
{
|
|
195
|
+
match: { prefix: ResourceTypePrefix.Blob },
|
|
196
|
+
rule: "always",
|
|
197
|
+
why:
|
|
198
|
+
"`ctl/file/blobInfo` is re-pointed for the same content: the cache holds no KV, and a " +
|
|
199
|
+
"tree reads only its `sizeBytes`, invariant for the same content",
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
match: { prefix: ResourceTypePrefix.BlobIndex },
|
|
203
|
+
rule: "readyAndAllOutputsFilled",
|
|
204
|
+
why: "`ctl/ctlsdk/bootstrapDone` is backend bookkeeping KV; no client reads it",
|
|
205
|
+
stopRule: earlyOnFieldErrorsWithOutputs,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
match: { prefix: ResourceTypePrefix.BlobUpload },
|
|
209
|
+
rule: "readyAndAllOutputsFilled",
|
|
210
|
+
why: "`ctl/file/storage/uploadState` is backend bookkeeping KV; no client reads it",
|
|
211
|
+
stopRule: earlyOnFieldErrorsWithOutputs,
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
match: { prefix: ResourceTypePrefix.BlobCopy },
|
|
215
|
+
rule: "readyAndAllOutputsFilled",
|
|
216
|
+
why:
|
|
217
|
+
"outputs are locked at creation; the only later write, the bootstrap's " +
|
|
218
|
+
"`ctl/ctlsdk/bootstrapDone`, is backend bookkeeping KV; no client reads it",
|
|
219
|
+
stopRule: earlyOnFieldErrorsWithOutputs,
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
match: { name: ResourceTypeName.WorkingDirectory },
|
|
223
|
+
rule: "always",
|
|
224
|
+
why: "the lock KV is backend bookkeeping; no client reads it",
|
|
225
|
+
},
|
|
226
|
+
]);
|
|
227
|
+
|
|
228
|
+
function streamSwitchedOrErrored(r: Parameters<FinalResourceDataPredicate>[0]): boolean {
|
|
29
229
|
if (!readyOrDuplicateOrError(r)) return false;
|
|
30
|
-
if (
|
|
31
|
-
if (r.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
230
|
+
if (r.fields === undefined) return true;
|
|
231
|
+
if (isNotNullSignedResourceId(r.error)) return true;
|
|
232
|
+
// Fields can be pruned away by the reader: without them nothing proves the switch. The
|
|
233
|
+
// predicate must not throw, since a throw inside a tree update rebuilds the whole tree.
|
|
234
|
+
const downloadable = r.fields.find((f) => f.name === "downloadable");
|
|
235
|
+
const stream = r.fields.find((f) => f.name === "stream");
|
|
236
|
+
if (downloadable === undefined || stream === undefined || isNullSignedResourceId(stream.value))
|
|
237
|
+
return false;
|
|
238
|
+
// No backend flag marks the switch: equal non-null fields mean the controller's success branch
|
|
239
|
+
// has switched `stream` to the downloadable blob.
|
|
240
|
+
return stream.value === downloadable.value;
|
|
36
241
|
}
|
|
37
242
|
|
|
38
|
-
|
|
39
|
-
|
|
243
|
+
/**
|
|
244
|
+
* The finality of trees, and the default of `PlClient.finalPredicate`: a resource it calls final
|
|
245
|
+
* leaves the refresh seeds, later bodies for it are ignored, and its mutable-state change sources
|
|
246
|
+
* are retired. It adds Blob and StreamManager under reader assumptions stated at each entry; a
|
|
247
|
+
* consumer that reads what they exclude passes its own predicate.
|
|
248
|
+
*/
|
|
249
|
+
export const TreeFinality: FinalityTable = CacheFinality.extend("tree", [
|
|
250
|
+
{
|
|
251
|
+
match: { name: ResourceTypeName.Blob },
|
|
252
|
+
rule: "always",
|
|
253
|
+
why:
|
|
254
|
+
"the incarnation field is replaced later; the project tree prunes Blob's fields, and the " +
|
|
255
|
+
"drivers read a blob by id, never through its fields",
|
|
256
|
+
requiresPruning: { type: ResourceTypeName.Blob, fields: "all" },
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
match: { name: ResourceTypeName.StreamManager },
|
|
260
|
+
rule: { custom: streamSwitchedOrErrored },
|
|
261
|
+
why:
|
|
262
|
+
"after an error `stream` is reset in a later transaction; default traversal raises the " +
|
|
263
|
+
"error before exposing the fields, so only an `ignoreError` read sees `stream`",
|
|
264
|
+
stopRule: {
|
|
265
|
+
approx: "readyOrDuplicateOrError",
|
|
266
|
+
reason: "a stop rule cannot compare two fields, so it stops before `stream` switches",
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
]);
|
|
40
270
|
|
|
41
|
-
/**
|
|
42
|
-
export const DefaultFinalResourceDataPredicate: FinalResourceDataPredicate =
|
|
43
|
-
|
|
44
|
-
case ResourceTypeName.StreamManager: {
|
|
45
|
-
if (!readyOrDuplicateOrError(r)) return false;
|
|
46
|
-
if (r.fields === undefined) return true; // if fields are not provided basic resource state is not expected to change in the future
|
|
47
|
-
if (isNotNullSignedResourceId(r.error)) return true;
|
|
48
|
-
// Fields can be pruned away by the reader: without them nothing proves the switch, so
|
|
49
|
-
// the resource is not final. The predicate must not throw, since a throw inside the
|
|
50
|
-
// tree's update invalidates and rebuilds the whole tree.
|
|
51
|
-
const downloadable = r.fields.find((f) => f.name === "downloadable");
|
|
52
|
-
const stream = r.fields.find((f) => f.name === "stream");
|
|
53
|
-
// Both still empty is not a switch either.
|
|
54
|
-
if (
|
|
55
|
-
downloadable === undefined ||
|
|
56
|
-
stream === undefined ||
|
|
57
|
-
isNullSignedResourceId(stream.value)
|
|
58
|
-
)
|
|
59
|
-
return false;
|
|
60
|
-
return stream.value === downloadable.value; // it's equal to the resource is marked as final on backend side
|
|
61
|
-
}
|
|
62
|
-
case ResourceTypeName.StdMap:
|
|
63
|
-
case ResourceTypeName.StdMapSlash:
|
|
64
|
-
case ResourceTypeName.EphStdMap:
|
|
65
|
-
case ResourceTypeName.PFrame:
|
|
66
|
-
case ResourceTypeName.ParquetChunk:
|
|
67
|
-
case ResourceTypeName.BContext:
|
|
68
|
-
case ResourceTypeName.BlockPackCustom:
|
|
69
|
-
case ResourceTypeName.BinaryMap:
|
|
70
|
-
case ResourceTypeName.BinaryValue:
|
|
71
|
-
case ResourceTypeName.BlobMap:
|
|
72
|
-
case ResourceTypeName.BResolveSingle:
|
|
73
|
-
case ResourceTypeName.BResolveSingleNoResult:
|
|
74
|
-
case ResourceTypeName.BQueryResult:
|
|
75
|
-
case ResourceTypeName.TengoTemplate:
|
|
76
|
-
case ResourceTypeName.TengoLib:
|
|
77
|
-
case ResourceTypeName.SoftwareInfo:
|
|
78
|
-
case ResourceTypeName.Dummy:
|
|
79
|
-
return readyOrDuplicateOrError(r);
|
|
80
|
-
case ResourceTypeName.JsonResourceError:
|
|
81
|
-
return r.type.version === "1";
|
|
82
|
-
case ResourceTypeName.JsonObject:
|
|
83
|
-
case ResourceTypeName.JsonGzObject:
|
|
84
|
-
case ResourceTypeName.JsonString:
|
|
85
|
-
case ResourceTypeName.JsonArray:
|
|
86
|
-
case ResourceTypeName.JsonNumber:
|
|
87
|
-
case ResourceTypeName.BContextEnd:
|
|
88
|
-
case ResourceTypeName.FrontendFromUrl:
|
|
89
|
-
case ResourceTypeName.FrontendFromFolder:
|
|
90
|
-
case ResourceTypeName.BObjectSpec:
|
|
91
|
-
case ResourceTypeName.Blob:
|
|
92
|
-
case ResourceTypeName.Null:
|
|
93
|
-
case ResourceTypeName.Binary:
|
|
94
|
-
case ResourceTypeName.LSProvider:
|
|
95
|
-
case ResourceTypeName.WorkingDirectory:
|
|
96
|
-
return true;
|
|
97
|
-
case ResourceTypeName.UserProject:
|
|
98
|
-
case ResourceTypeName.Projects:
|
|
99
|
-
case ResourceTypeName.ClientRoot:
|
|
100
|
-
// Never final — these sharing resources gain and lose dynamic child fields over their lifetime.
|
|
101
|
-
case ResourceTypeName.SharingOutbox:
|
|
102
|
-
case ResourceTypeName.SharingState:
|
|
103
|
-
case ResourceTypeName.SharedEnvelope:
|
|
104
|
-
return false;
|
|
105
|
-
default:
|
|
106
|
-
if (
|
|
107
|
-
r.type.name.startsWith(ResourceTypePrefix.Blob) ||
|
|
108
|
-
r.type.name.startsWith(ResourceTypePrefix.LS) ||
|
|
109
|
-
r.type.name.startsWith(ResourceTypePrefix.WorkingDirectory) ||
|
|
110
|
-
r.type.name.startsWith(ResourceTypePrefix.StorageSpaceAllocation)
|
|
111
|
-
) {
|
|
112
|
-
return true;
|
|
113
|
-
} else if (
|
|
114
|
-
r.type.name.startsWith(ResourceTypePrefix.BlobUpload) ||
|
|
115
|
-
r.type.name.startsWith(ResourceTypePrefix.BlobIndex)
|
|
116
|
-
) {
|
|
117
|
-
return readyAndHasAllOutputsFilled(r);
|
|
118
|
-
} else if (r.type.name.startsWith(ResourceTypePrefix.PColumnData)) {
|
|
119
|
-
return readyOrDuplicateOrError(r);
|
|
120
|
-
} else if (r.type.name.startsWith(ResourceTypePrefix.StreamWorkdir)) {
|
|
121
|
-
return readyOrDuplicateOrError(r);
|
|
122
|
-
} else {
|
|
123
|
-
// Unknown resource type detected
|
|
124
|
-
// Set used to log this message only once
|
|
125
|
-
if (!unknownResourceTypeNames.has(r.type.name)) {
|
|
126
|
-
console.log("UNKNOWN RESOURCE TYPE: " + r.type.name);
|
|
127
|
-
unknownResourceTypeNames.add(r.type.name);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return false;
|
|
132
|
-
};
|
|
271
|
+
/** @deprecated use {@link TreeFinality} */
|
|
272
|
+
export const DefaultFinalResourceDataPredicate: FinalResourceDataPredicate =
|
|
273
|
+
TreeFinality.predicate();
|