@milaboratories/pl-client 3.17.6 → 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.
@@ -1,77 +1,209 @@
1
- import { getField, isNotNullSignedResourceId, isNullSignedResourceId } from "./types.js";
1
+ import { isNotNullSignedResourceId, isNullSignedResourceId } from "./types.js";
2
+ import { FinalityTable, readyOrDuplicateOrError } from "./finality.js";
2
3
  import { ResourceTypeName, ResourceTypePrefix } from "@milaboratories/pl-model-common";
3
4
  //#region src/core/final.ts
4
- function readyOrDuplicateOrError(r) {
5
- return r.resourceReady || isNotNullSignedResourceId(r.originalResourceId) || isNotNullSignedResourceId(r.error);
6
- }
7
- function readyAndHasAllOutputsFilled(r) {
5
+ const earlyOnFieldErrors = {
6
+ approx: "readyOrDuplicateOrError",
7
+ reason: "HAS_ERRORS is set by an error on any field, the predicate reads the resource error only; an early stop is followed up with a plain fetch"
8
+ };
9
+ const earlyOnFieldErrorsWithOutputs = {
10
+ approx: "readyAndAllOutputsFilled",
11
+ reason: "HAS_ERRORS is set by an error on any field, the predicate reads the resource error only; ALL_OUTPUTS_FINAL, like the predicate, checks every retained field; an early stop is followed up with a plain fetch"
12
+ };
13
+ const value = (name) => ({
14
+ match: { name },
15
+ rule: "always",
16
+ why: "a value, written once at creation"
17
+ });
18
+ const settledAtReady = (name, why) => ({
19
+ match: { name },
20
+ rule: "readyOrDuplicateOrError",
21
+ why,
22
+ stopRule: earlyOnFieldErrors
23
+ });
24
+ const never = (match, why) => ({
25
+ match,
26
+ rule: "never",
27
+ why
28
+ });
29
+ /** The base layer: nothing observable (state, fields, KV) changes after the rule holds, whatever
30
+ * the consumer prunes. Exceptions: the data-loss error cascade, the `hasOriginalListeners` flag,
31
+ * and the writes its entries name. */
32
+ const StrictFinality = FinalityTable.of("strict", [
33
+ value(ResourceTypeName.JsonObject),
34
+ value(ResourceTypeName.JsonGzObject),
35
+ value(ResourceTypeName.JsonString),
36
+ value(ResourceTypeName.JsonArray),
37
+ value(ResourceTypeName.JsonNumber),
38
+ value(ResourceTypeName.JsonBool),
39
+ value(ResourceTypeName.JsonNull),
40
+ {
41
+ match: { name: ResourceTypeName.JsonErrorTrace },
42
+ rule: "always",
43
+ why: "an error-trace snapshot, populated and locked in its creating transaction"
44
+ },
45
+ value(ResourceTypeName.BContextEnd),
46
+ value(ResourceTypeName.FrontendFromUrl),
47
+ value(ResourceTypeName.FrontendFromFolder),
48
+ value(ResourceTypeName.FrontendFromLocalTgz),
49
+ value(ResourceTypeName.BObjectSpec),
50
+ value(ResourceTypeName.Null),
51
+ value(ResourceTypeName.Binary),
52
+ {
53
+ match: { name: ResourceTypeName.JsonResourceError },
54
+ rule: { custom: (r) => r.type.version === "1" },
55
+ why: "version 1 is a value, written once at creation",
56
+ stopRule: {
57
+ approx: "always",
58
+ reason: "a stop rule cannot see the type version"
59
+ }
60
+ },
61
+ {
62
+ match: { prefix: ResourceTypePrefix.LS },
63
+ rule: "always",
64
+ why: "no write after creation"
65
+ },
66
+ {
67
+ match: { prefix: ResourceTypePrefix.WorkingDirectory },
68
+ rule: "always",
69
+ why: "no write after creation"
70
+ },
71
+ {
72
+ match: { prefix: ResourceTypePrefix.StorageSpaceAllocation },
73
+ rule: "always",
74
+ why: "no write after creation"
75
+ },
76
+ ...[
77
+ ResourceTypeName.StdMap,
78
+ ResourceTypeName.StdMapSlash,
79
+ ResourceTypeName.EphStdMap,
80
+ ResourceTypeName.PFrame,
81
+ ResourceTypeName.ParquetChunk,
82
+ ResourceTypeName.BContext,
83
+ ResourceTypeName.BinaryMap,
84
+ ResourceTypeName.BinaryValue,
85
+ ResourceTypeName.BlobMap,
86
+ ResourceTypeName.BResolveSingleNoResult,
87
+ ResourceTypeName.BQueryResult,
88
+ ResourceTypeName.TengoLib
89
+ ].map((name) => settledAtReady(name, "its inputs are final at ready, and no controller fills it afterwards")),
90
+ ...[
91
+ ResourceTypeName.TengoTemplate,
92
+ ResourceTypeName.SoftwareInfo,
93
+ ResourceTypeName.BlockPackCustom
94
+ ].map((name) => settledAtReady(name, "its only later write is the `hasOriginalListeners` flag its first duplicate sets")),
95
+ settledAtReady(ResourceTypeName.Dummy, "its only later write is a data-loss error from the Blob whose incarnation field holds it"),
96
+ {
97
+ match: { prefix: ResourceTypePrefix.PColumnData },
98
+ rule: "readyOrDuplicateOrError",
99
+ why: "its inputs are final at ready, and no controller fills it afterwards",
100
+ stopRule: earlyOnFieldErrors
101
+ },
102
+ {
103
+ match: { prefix: ResourceTypePrefix.StreamWorkdir },
104
+ rule: "readyOrDuplicateOrError",
105
+ why: "no write after ready",
106
+ stopRule: {
107
+ approx: "readyOrDuplicateOrError",
108
+ reason: "HAS_ERRORS is set by an error on any field; on ResourceTree walks the project field filter also removes every field, `resourceError` included, before decoding, so the predicate cannot see an error the filter stops on; an early stop is followed up with a plain fetch"
109
+ }
110
+ },
111
+ {
112
+ match: { name: ResourceTypeName.BResolveSingle },
113
+ rule: "readyAndAllOutputsFilled",
114
+ why: "outputs are locked at creation and assigned after ready, never overwritten; one exception: a later context with more than one match sets an error on a resolver that already succeeded, and a reader holding it final misses that error",
115
+ stopRule: earlyOnFieldErrorsWithOutputs
116
+ },
117
+ {
118
+ match: { name: ResourceTypeName.BResolveChoice },
119
+ rule: "readyAndAllOutputsFilled",
120
+ why: "outputs are locked at creation and point at result maps that are completed and locked as resolution finishes",
121
+ stopRule: earlyOnFieldErrorsWithOutputs
122
+ },
123
+ never({ name: ResourceTypeName.UserProject }, "blocks and fields come and go over its life"),
124
+ never({ name: ResourceTypeName.Projects }, "projects are added and removed"),
125
+ never({ name: ResourceTypeName.ClientRoot }, "its fields are added and removed"),
126
+ ...[
127
+ ResourceTypeName.SharingOutbox,
128
+ ResourceTypeName.SharingState,
129
+ ResourceTypeName.SharedEnvelope
130
+ ].map((name) => never({ name }, "gains and loses dynamic child fields over its life")),
131
+ never({ name: ResourceTypeName.LSProvider }, "its storage fields are rewritten on a controller restart with a changed storage config"),
132
+ never({ name: ResourceTypeName.Blob }, "its incarnation field is attached or replaced later"),
133
+ never({ name: ResourceTypeName.StreamManager }, "`stream` is reset after an error"),
134
+ never({ prefix: ResourceTypePrefix.Blob }, "its `ctl/file/blobInfo` KV is re-pointed later"),
135
+ never({ prefix: ResourceTypePrefix.BlobIndex }, "a controller restart writes its `ctl/ctlsdk/bootstrapDone` KV"),
136
+ never({ prefix: ResourceTypePrefix.BlobUpload }, "the Reset RPC deletes its `ctl/file/storage/uploadState` KV"),
137
+ never({ prefix: ResourceTypePrefix.BlobCopy }, "a controller bootstrap records its `ctl/ctlsdk/bootstrapDone` KV in a separate transaction, which may run after the copy settled"),
138
+ never({ name: ResourceTypeName.WorkingDirectory }, "lock acquisition can write its `internal/locks/lockedBy` KV after creation")
139
+ ]);
140
+ /** What the transaction resource cache may keep while an entry stays in its LRU. The cache holds
141
+ * state and fields, never KV, so it adds the types whose only later writes are KV. */
142
+ const CacheFinality = StrictFinality.extend("cache", [
143
+ {
144
+ match: { prefix: ResourceTypePrefix.Blob },
145
+ rule: "always",
146
+ why: "`ctl/file/blobInfo` is re-pointed for the same content: the cache holds no KV, and a tree reads only its `sizeBytes`, invariant for the same content"
147
+ },
148
+ {
149
+ match: { prefix: ResourceTypePrefix.BlobIndex },
150
+ rule: "readyAndAllOutputsFilled",
151
+ why: "`ctl/ctlsdk/bootstrapDone` is backend bookkeeping KV; no client reads it",
152
+ stopRule: earlyOnFieldErrorsWithOutputs
153
+ },
154
+ {
155
+ match: { prefix: ResourceTypePrefix.BlobUpload },
156
+ rule: "readyAndAllOutputsFilled",
157
+ why: "`ctl/file/storage/uploadState` is backend bookkeeping KV; no client reads it",
158
+ stopRule: earlyOnFieldErrorsWithOutputs
159
+ },
160
+ {
161
+ match: { prefix: ResourceTypePrefix.BlobCopy },
162
+ rule: "readyAndAllOutputsFilled",
163
+ why: "outputs are locked at creation; the only later write, the bootstrap's `ctl/ctlsdk/bootstrapDone`, is backend bookkeeping KV; no client reads it",
164
+ stopRule: earlyOnFieldErrorsWithOutputs
165
+ },
166
+ {
167
+ match: { name: ResourceTypeName.WorkingDirectory },
168
+ rule: "always",
169
+ why: "the lock KV is backend bookkeeping; no client reads it"
170
+ }
171
+ ]);
172
+ function streamSwitchedOrErrored(r) {
8
173
  if (!readyOrDuplicateOrError(r)) return false;
9
- if (!r.outputsLocked) return false;
10
174
  if (r.fields === void 0) return true;
11
- for (const f of r.fields) if (isNullSignedResourceId(f.error) && (isNullSignedResourceId(f.value) || !f.valueIsFinal)) return false;
12
- return true;
175
+ if (isNotNullSignedResourceId(r.error)) return true;
176
+ const downloadable = r.fields.find((f) => f.name === "downloadable");
177
+ const stream = r.fields.find((f) => f.name === "stream");
178
+ if (downloadable === void 0 || stream === void 0 || isNullSignedResourceId(stream.value)) return false;
179
+ return stream.value === downloadable.value;
13
180
  }
14
- const unknownResourceTypeNames = /* @__PURE__ */ new Set();
15
- /** Default implementation, defining behaviour for built-in resource types. */
16
- const DefaultFinalResourceDataPredicate = (r) => {
17
- switch (r.type.name) {
18
- case ResourceTypeName.StreamManager: {
19
- if (!readyOrDuplicateOrError(r)) return false;
20
- if (r.fields === void 0) return true;
21
- if (isNotNullSignedResourceId(r.error)) return true;
22
- const downloadable = getField(r, "downloadable");
23
- return getField(r, "stream").value === downloadable.value;
24
- }
25
- case ResourceTypeName.StdMap:
26
- case ResourceTypeName.StdMapSlash:
27
- case ResourceTypeName.EphStdMap:
28
- case ResourceTypeName.PFrame:
29
- case ResourceTypeName.ParquetChunk:
30
- case ResourceTypeName.BContext:
31
- case ResourceTypeName.BlockPackCustom:
32
- case ResourceTypeName.BinaryMap:
33
- case ResourceTypeName.BinaryValue:
34
- case ResourceTypeName.BlobMap:
35
- case ResourceTypeName.BResolveSingle:
36
- case ResourceTypeName.BResolveSingleNoResult:
37
- case ResourceTypeName.BQueryResult:
38
- case ResourceTypeName.TengoTemplate:
39
- case ResourceTypeName.TengoLib:
40
- case ResourceTypeName.SoftwareInfo:
41
- case ResourceTypeName.Dummy: return readyOrDuplicateOrError(r);
42
- case ResourceTypeName.JsonResourceError: return r.type.version === "1";
43
- case ResourceTypeName.JsonObject:
44
- case ResourceTypeName.JsonGzObject:
45
- case ResourceTypeName.JsonString:
46
- case ResourceTypeName.JsonArray:
47
- case ResourceTypeName.JsonNumber:
48
- case ResourceTypeName.BContextEnd:
49
- case ResourceTypeName.FrontendFromUrl:
50
- case ResourceTypeName.FrontendFromFolder:
51
- case ResourceTypeName.BObjectSpec:
52
- case ResourceTypeName.Blob:
53
- case ResourceTypeName.Null:
54
- case ResourceTypeName.Binary:
55
- case ResourceTypeName.LSProvider:
56
- case ResourceTypeName.WorkingDirectory: return true;
57
- case ResourceTypeName.UserProject:
58
- case ResourceTypeName.Projects:
59
- case ResourceTypeName.ClientRoot:
60
- case ResourceTypeName.SharingOutbox:
61
- case ResourceTypeName.SharingState:
62
- case ResourceTypeName.SharedEnvelope: return false;
63
- default: if (r.type.name.startsWith(ResourceTypePrefix.Blob) || r.type.name.startsWith(ResourceTypePrefix.LS) || r.type.name.startsWith(ResourceTypePrefix.WorkingDirectory) || r.type.name.startsWith(ResourceTypePrefix.StorageSpaceAllocation)) return true;
64
- else if (r.type.name.startsWith(ResourceTypePrefix.BlobUpload) || r.type.name.startsWith(ResourceTypePrefix.BlobIndex)) return readyAndHasAllOutputsFilled(r);
65
- else if (r.type.name.startsWith(ResourceTypePrefix.PColumnData)) return readyOrDuplicateOrError(r);
66
- else if (r.type.name.startsWith(ResourceTypePrefix.StreamWorkdir)) return readyOrDuplicateOrError(r);
67
- else if (!unknownResourceTypeNames.has(r.type.name)) {
68
- console.log("UNKNOWN RESOURCE TYPE: " + r.type.name);
69
- unknownResourceTypeNames.add(r.type.name);
70
- }
181
+ /**
182
+ * The finality of trees, and the default of `PlClient.finalPredicate`: a resource it calls final
183
+ * leaves the refresh seeds, later bodies for it are ignored, and its mutable-state change sources
184
+ * are retired. It adds Blob and StreamManager under reader assumptions stated at each entry; a
185
+ * consumer that reads what they exclude passes its own predicate.
186
+ */
187
+ const TreeFinality = CacheFinality.extend("tree", [{
188
+ match: { name: ResourceTypeName.Blob },
189
+ rule: "always",
190
+ why: "the incarnation field is replaced later; the project tree prunes Blob's fields, and the drivers read a blob by id, never through its fields",
191
+ requiresPruning: {
192
+ type: ResourceTypeName.Blob,
193
+ fields: "all"
71
194
  }
72
- return false;
73
- };
195
+ }, {
196
+ match: { name: ResourceTypeName.StreamManager },
197
+ rule: { custom: streamSwitchedOrErrored },
198
+ why: "after an error `stream` is reset in a later transaction; default traversal raises the error before exposing the fields, so only an `ignoreError` read sees `stream`",
199
+ stopRule: {
200
+ approx: "readyOrDuplicateOrError",
201
+ reason: "a stop rule cannot compare two fields, so it stops before `stream` switches"
202
+ }
203
+ }]);
204
+ /** @deprecated use {@link TreeFinality} */
205
+ const DefaultFinalResourceDataPredicate = TreeFinality.predicate();
74
206
  //#endregion
75
- export { DefaultFinalResourceDataPredicate, ResourceTypeName, ResourceTypePrefix, readyOrDuplicateOrError };
207
+ export { CacheFinality, DefaultFinalResourceDataPredicate, ResourceTypeName, ResourceTypePrefix, StrictFinality, TreeFinality, readyOrDuplicateOrError };
76
208
 
77
209
  //# sourceMappingURL=final.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"final.js","names":[],"sources":["../../src/core/final.ts"],"sourcesContent":["import type { Optional } from \"utility-types\";\nimport type { BasicResourceData, ResourceData } from \"./types\";\nimport { getField, isNotNullSignedResourceId, isNullSignedResourceId } from \"./types\";\nimport { ResourceTypeName, ResourceTypePrefix } from \"@milaboratories/pl-model-common\";\nexport { ResourceTypeName, ResourceTypePrefix };\n\n/**\n * Function is used to guide multiple layers of caching in pl-client and derived pl-tree.\n *\n * This function defines expected resource-specific state mutation behaviour,\n * if it returns true, system will expect that this data will never change as long as resource exist.\n *\n * If resource data contain information about fields, if should be taken into account, fields are undefined,\n * \"final\" state should be calculated for \"basic\" part of resource data only.\n */\nexport type FinalResourceDataPredicate = (\n resourceData: Optional<ResourceData, \"fields\">,\n) => boolean;\n\nexport function readyOrDuplicateOrError(r: ResourceData | BasicResourceData): boolean {\n return (\n r.resourceReady ||\n isNotNullSignedResourceId(r.originalResourceId) ||\n isNotNullSignedResourceId(r.error)\n );\n}\n\nfunction readyAndHasAllOutputsFilled(r: Optional<ResourceData, \"fields\">): boolean {\n if (!readyOrDuplicateOrError(r)) return false;\n if (!r.outputsLocked) return false;\n if (r.fields === undefined) return true; // if fields are not provided basic resource state is not expected to change in the future\n for (const f of r.fields)\n if (isNullSignedResourceId(f.error) && (isNullSignedResourceId(f.value) || !f.valueIsFinal))\n return false;\n return true;\n}\n\n// solely for logging\nconst unknownResourceTypeNames = new Set<string>();\n\n/** Default implementation, defining behaviour for built-in resource types. */\nexport const DefaultFinalResourceDataPredicate: FinalResourceDataPredicate = (r): boolean => {\n switch (r.type.name) {\n case ResourceTypeName.StreamManager: {\n if (!readyOrDuplicateOrError(r)) return false;\n if (r.fields === undefined) return true; // if fields are not provided basic resource state is not expected to change in the future\n if (isNotNullSignedResourceId(r.error)) return true;\n const downloadable = getField(r as ResourceData, \"downloadable\");\n const stream = getField(r as ResourceData, \"stream\");\n return stream.value === downloadable.value; // it's equal to the resource is marked as final on backend side\n }\n case ResourceTypeName.StdMap:\n case ResourceTypeName.StdMapSlash:\n case ResourceTypeName.EphStdMap:\n case ResourceTypeName.PFrame:\n case ResourceTypeName.ParquetChunk:\n case ResourceTypeName.BContext:\n case ResourceTypeName.BlockPackCustom:\n case ResourceTypeName.BinaryMap:\n case ResourceTypeName.BinaryValue:\n case ResourceTypeName.BlobMap:\n case ResourceTypeName.BResolveSingle:\n case ResourceTypeName.BResolveSingleNoResult:\n case ResourceTypeName.BQueryResult:\n case ResourceTypeName.TengoTemplate:\n case ResourceTypeName.TengoLib:\n case ResourceTypeName.SoftwareInfo:\n case ResourceTypeName.Dummy:\n return readyOrDuplicateOrError(r);\n case ResourceTypeName.JsonResourceError:\n return r.type.version === \"1\";\n case ResourceTypeName.JsonObject:\n case ResourceTypeName.JsonGzObject:\n case ResourceTypeName.JsonString:\n case ResourceTypeName.JsonArray:\n case ResourceTypeName.JsonNumber:\n case ResourceTypeName.BContextEnd:\n case ResourceTypeName.FrontendFromUrl:\n case ResourceTypeName.FrontendFromFolder:\n case ResourceTypeName.BObjectSpec:\n case ResourceTypeName.Blob:\n case ResourceTypeName.Null:\n case ResourceTypeName.Binary:\n case ResourceTypeName.LSProvider:\n case ResourceTypeName.WorkingDirectory:\n return true;\n case ResourceTypeName.UserProject:\n case ResourceTypeName.Projects:\n case ResourceTypeName.ClientRoot:\n // Never final — these sharing resources gain and lose dynamic child fields over their lifetime.\n case ResourceTypeName.SharingOutbox:\n case ResourceTypeName.SharingState:\n case ResourceTypeName.SharedEnvelope:\n return false;\n default:\n if (\n r.type.name.startsWith(ResourceTypePrefix.Blob) ||\n r.type.name.startsWith(ResourceTypePrefix.LS) ||\n r.type.name.startsWith(ResourceTypePrefix.WorkingDirectory) ||\n r.type.name.startsWith(ResourceTypePrefix.StorageSpaceAllocation)\n ) {\n return true;\n } else if (\n r.type.name.startsWith(ResourceTypePrefix.BlobUpload) ||\n r.type.name.startsWith(ResourceTypePrefix.BlobIndex)\n ) {\n return readyAndHasAllOutputsFilled(r);\n } else if (r.type.name.startsWith(ResourceTypePrefix.PColumnData)) {\n return readyOrDuplicateOrError(r);\n } else if (r.type.name.startsWith(ResourceTypePrefix.StreamWorkdir)) {\n return readyOrDuplicateOrError(r);\n } else {\n // Unknown resource type detected\n // Set used to log this message only once\n if (!unknownResourceTypeNames.has(r.type.name)) {\n console.log(\"UNKNOWN RESOURCE TYPE: \" + r.type.name);\n unknownResourceTypeNames.add(r.type.name);\n }\n }\n }\n return false;\n};\n"],"mappings":";;;AAmBA,SAAgB,wBAAwB,GAA8C;CACpF,OACE,EAAE,iBACF,0BAA0B,EAAE,kBAAkB,KAC9C,0BAA0B,EAAE,KAAK;AAErC;AAEA,SAAS,4BAA4B,GAA8C;CACjF,IAAI,CAAC,wBAAwB,CAAC,GAAG,OAAO;CACxC,IAAI,CAAC,EAAE,eAAe,OAAO;CAC7B,IAAI,EAAE,WAAW,KAAA,GAAW,OAAO;CACnC,KAAK,MAAM,KAAK,EAAE,QAChB,IAAI,uBAAuB,EAAE,KAAK,MAAM,uBAAuB,EAAE,KAAK,KAAK,CAAC,EAAE,eAC5E,OAAO;CACX,OAAO;AACT;AAGA,MAAM,2CAA2B,IAAI,IAAY;;AAGjD,MAAa,qCAAiE,MAAe;CAC3F,QAAQ,EAAE,KAAK,MAAf;EACE,KAAK,iBAAiB,eAAe;GACnC,IAAI,CAAC,wBAAwB,CAAC,GAAG,OAAO;GACxC,IAAI,EAAE,WAAW,KAAA,GAAW,OAAO;GACnC,IAAI,0BAA0B,EAAE,KAAK,GAAG,OAAO;GAC/C,MAAM,eAAe,SAAS,GAAmB,cAAc;GAE/D,OADe,SAAS,GAAmB,QAC/B,CAAC,CAAC,UAAU,aAAa;EACvC;EACA,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB,OACpB,OAAO,wBAAwB,CAAC;EAClC,KAAK,iBAAiB,mBACpB,OAAO,EAAE,KAAK,YAAY;EAC5B,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB,kBACpB,OAAO;EACT,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EAEtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB;EACtB,KAAK,iBAAiB,gBACpB,OAAO;EACT,SACE,IACE,EAAE,KAAK,KAAK,WAAW,mBAAmB,IAAI,KAC9C,EAAE,KAAK,KAAK,WAAW,mBAAmB,EAAE,KAC5C,EAAE,KAAK,KAAK,WAAW,mBAAmB,gBAAgB,KAC1D,EAAE,KAAK,KAAK,WAAW,mBAAmB,sBAAsB,GAEhE,OAAO;OACF,IACL,EAAE,KAAK,KAAK,WAAW,mBAAmB,UAAU,KACpD,EAAE,KAAK,KAAK,WAAW,mBAAmB,SAAS,GAEnD,OAAO,4BAA4B,CAAC;OAC/B,IAAI,EAAE,KAAK,KAAK,WAAW,mBAAmB,WAAW,GAC9D,OAAO,wBAAwB,CAAC;OAC3B,IAAI,EAAE,KAAK,KAAK,WAAW,mBAAmB,aAAa,GAChE,OAAO,wBAAwB,CAAC;OAIhC,IAAI,CAAC,yBAAyB,IAAI,EAAE,KAAK,IAAI,GAAG;GAC9C,QAAQ,IAAI,4BAA4B,EAAE,KAAK,IAAI;GACnD,yBAAyB,IAAI,EAAE,KAAK,IAAI;EAC1C;CAEN;CACA,OAAO;AACT"}
1
+ {"version":3,"file":"final.js","names":[],"sources":["../../src/core/final.ts"],"sourcesContent":["import { ResourceTypeName, ResourceTypePrefix } from \"@milaboratories/pl-model-common\";\nimport type { FinalityEntry, FinalityStopRule, FinalResourceDataPredicate } from \"./finality\";\nimport { FinalityTable, readyOrDuplicateOrError } from \"./finality\";\nimport { isNotNullSignedResourceId, isNullSignedResourceId } from \"./types\";\nexport { ResourceTypeName, ResourceTypePrefix };\nexport type { FinalResourceDataPredicate } from \"./finality\";\nexport { readyOrDuplicateOrError } from \"./finality\";\n\n// The finality tables, one layer per consumer. Every layer accepts two backend writes after\n// final: a data-loss error cascade, lifting the error of a stored blob found lost or corrupt to\n// the resources built from it, and the first duplicate's `hasOriginalListeners` flag on its\n// original, backend-internal and never on the wire. An entry's `why` names any other write\n// it accepts. A layer adds only the types whose later writes its consumer does not observe.\n\n// Where the backend's filter stops earlier than the predicate. An early stop is safe: the\n// backend sends a body-less stop marker, and the streaming loader fetches every stopped resource\n// it does not hold as final again without stop rules (field filtering and pruning still apply),\n// so the predicate decides on the fetched body.\nconst earlyOnFieldErrors: FinalityStopRule = {\n approx: \"readyOrDuplicateOrError\",\n reason:\n \"HAS_ERRORS is set by an error on any field, the predicate reads the resource error only; \" +\n \"an early stop is followed up with a plain fetch\",\n};\nconst earlyOnFieldErrorsWithOutputs: FinalityStopRule = {\n approx: \"readyAndAllOutputsFilled\",\n reason:\n \"HAS_ERRORS is set by an error on any field, the predicate reads the resource error only; \" +\n \"ALL_OUTPUTS_FINAL, like the predicate, checks every retained field; an early stop is \" +\n \"followed up with a plain fetch\",\n};\n\nconst value = (name: string): FinalityEntry => ({\n match: { name },\n rule: \"always\",\n why: \"a value, written once at creation\",\n});\n\nconst settledAtReady = (name: string, why: string): FinalityEntry => ({\n match: { name },\n rule: \"readyOrDuplicateOrError\",\n why,\n stopRule: earlyOnFieldErrors,\n});\n\nconst never = (match: FinalityEntry[\"match\"], why: string): FinalityEntry => ({\n match,\n rule: \"never\",\n why,\n});\n\n/** The base layer: nothing observable (state, fields, KV) changes after the rule holds, whatever\n * the consumer prunes. Exceptions: the data-loss error cascade, the `hasOriginalListeners` flag,\n * and the writes its entries name. */\nexport const StrictFinality: FinalityTable = FinalityTable.of(\"strict\", [\n value(ResourceTypeName.JsonObject),\n value(ResourceTypeName.JsonGzObject),\n value(ResourceTypeName.JsonString),\n value(ResourceTypeName.JsonArray),\n value(ResourceTypeName.JsonNumber),\n value(ResourceTypeName.JsonBool),\n value(ResourceTypeName.JsonNull),\n {\n match: { name: ResourceTypeName.JsonErrorTrace },\n rule: \"always\",\n why: \"an error-trace snapshot, populated and locked in its creating transaction\",\n },\n value(ResourceTypeName.BContextEnd),\n value(ResourceTypeName.FrontendFromUrl),\n value(ResourceTypeName.FrontendFromFolder),\n value(ResourceTypeName.FrontendFromLocalTgz),\n value(ResourceTypeName.BObjectSpec),\n value(ResourceTypeName.Null),\n value(ResourceTypeName.Binary),\n {\n match: { name: ResourceTypeName.JsonResourceError },\n rule: { custom: (r) => r.type.version === \"1\" },\n why: \"version 1 is a value, written once at creation\",\n stopRule: { approx: \"always\", reason: \"a stop rule cannot see the type version\" },\n },\n { match: { prefix: ResourceTypePrefix.LS }, rule: \"always\", why: \"no write after creation\" },\n {\n match: { prefix: ResourceTypePrefix.WorkingDirectory },\n rule: \"always\",\n why: \"no write after creation\",\n },\n {\n match: { prefix: ResourceTypePrefix.StorageSpaceAllocation },\n rule: \"always\",\n why: \"no write after creation\",\n },\n ...[\n ResourceTypeName.StdMap,\n ResourceTypeName.StdMapSlash,\n ResourceTypeName.EphStdMap,\n ResourceTypeName.PFrame,\n ResourceTypeName.ParquetChunk,\n ResourceTypeName.BContext,\n ResourceTypeName.BinaryMap,\n ResourceTypeName.BinaryValue,\n ResourceTypeName.BlobMap,\n ResourceTypeName.BResolveSingleNoResult,\n ResourceTypeName.BQueryResult,\n ResourceTypeName.TengoLib,\n ].map((name) =>\n settledAtReady(name, \"its inputs are final at ready, and no controller fills it afterwards\"),\n ),\n ...[\n ResourceTypeName.TengoTemplate,\n ResourceTypeName.SoftwareInfo,\n ResourceTypeName.BlockPackCustom,\n ].map((name) =>\n settledAtReady(\n name,\n \"its only later write is the `hasOriginalListeners` flag its first duplicate sets\",\n ),\n ),\n settledAtReady(\n ResourceTypeName.Dummy,\n \"its only later write is a data-loss error from the Blob whose incarnation field holds it\",\n ),\n {\n match: { prefix: ResourceTypePrefix.PColumnData },\n rule: \"readyOrDuplicateOrError\",\n why: \"its inputs are final at ready, and no controller fills it afterwards\",\n stopRule: earlyOnFieldErrors,\n },\n {\n match: { prefix: ResourceTypePrefix.StreamWorkdir },\n rule: \"readyOrDuplicateOrError\",\n why: \"no write after ready\",\n stopRule: {\n approx: \"readyOrDuplicateOrError\",\n reason:\n \"HAS_ERRORS is set by an error on any field; on ResourceTree walks the project field \" +\n \"filter also removes every field, `resourceError` included, before decoding, so the \" +\n \"predicate cannot see an error the filter stops on; an early stop is followed up with \" +\n \"a plain fetch\",\n },\n },\n {\n match: { name: ResourceTypeName.BResolveSingle },\n rule: \"readyAndAllOutputsFilled\",\n why:\n \"outputs are locked at creation and assigned after ready, never overwritten; one \" +\n \"exception: a later context with more than one match sets an error on a resolver that \" +\n \"already succeeded, and a reader holding it final misses that error\",\n stopRule: earlyOnFieldErrorsWithOutputs,\n },\n {\n match: { name: ResourceTypeName.BResolveChoice },\n rule: \"readyAndAllOutputsFilled\",\n why:\n \"outputs are locked at creation and point at result maps that are completed and locked \" +\n \"as resolution finishes\",\n stopRule: earlyOnFieldErrorsWithOutputs,\n },\n never({ name: ResourceTypeName.UserProject }, \"blocks and fields come and go over its life\"),\n never({ name: ResourceTypeName.Projects }, \"projects are added and removed\"),\n never({ name: ResourceTypeName.ClientRoot }, \"its fields are added and removed\"),\n ...[\n ResourceTypeName.SharingOutbox,\n ResourceTypeName.SharingState,\n ResourceTypeName.SharedEnvelope,\n ].map((name) => never({ name }, \"gains and loses dynamic child fields over its life\")),\n never(\n { name: ResourceTypeName.LSProvider },\n \"its storage fields are rewritten on a controller restart with a changed storage config\",\n ),\n never({ name: ResourceTypeName.Blob }, \"its incarnation field is attached or replaced later\"),\n never({ name: ResourceTypeName.StreamManager }, \"`stream` is reset after an error\"),\n never({ prefix: ResourceTypePrefix.Blob }, \"its `ctl/file/blobInfo` KV is re-pointed later\"),\n never(\n { prefix: ResourceTypePrefix.BlobIndex },\n \"a controller restart writes its `ctl/ctlsdk/bootstrapDone` KV\",\n ),\n never(\n { prefix: ResourceTypePrefix.BlobUpload },\n \"the Reset RPC deletes its `ctl/file/storage/uploadState` KV\",\n ),\n never(\n { prefix: ResourceTypePrefix.BlobCopy },\n \"a controller bootstrap records its `ctl/ctlsdk/bootstrapDone` KV in a separate transaction, which may run after the copy settled\",\n ),\n never(\n { name: ResourceTypeName.WorkingDirectory },\n \"lock acquisition can write its `internal/locks/lockedBy` KV after creation\",\n ),\n]);\n\n/** What the transaction resource cache may keep while an entry stays in its LRU. The cache holds\n * state and fields, never KV, so it adds the types whose only later writes are KV. */\nexport const CacheFinality: FinalityTable = StrictFinality.extend(\"cache\", [\n {\n match: { prefix: ResourceTypePrefix.Blob },\n rule: \"always\",\n why:\n \"`ctl/file/blobInfo` is re-pointed for the same content: the cache holds no KV, and a \" +\n \"tree reads only its `sizeBytes`, invariant for the same content\",\n },\n {\n match: { prefix: ResourceTypePrefix.BlobIndex },\n rule: \"readyAndAllOutputsFilled\",\n why: \"`ctl/ctlsdk/bootstrapDone` is backend bookkeeping KV; no client reads it\",\n stopRule: earlyOnFieldErrorsWithOutputs,\n },\n {\n match: { prefix: ResourceTypePrefix.BlobUpload },\n rule: \"readyAndAllOutputsFilled\",\n why: \"`ctl/file/storage/uploadState` is backend bookkeeping KV; no client reads it\",\n stopRule: earlyOnFieldErrorsWithOutputs,\n },\n {\n match: { prefix: ResourceTypePrefix.BlobCopy },\n rule: \"readyAndAllOutputsFilled\",\n why:\n \"outputs are locked at creation; the only later write, the bootstrap's \" +\n \"`ctl/ctlsdk/bootstrapDone`, is backend bookkeeping KV; no client reads it\",\n stopRule: earlyOnFieldErrorsWithOutputs,\n },\n {\n match: { name: ResourceTypeName.WorkingDirectory },\n rule: \"always\",\n why: \"the lock KV is backend bookkeeping; no client reads it\",\n },\n]);\n\nfunction streamSwitchedOrErrored(r: Parameters<FinalResourceDataPredicate>[0]): boolean {\n if (!readyOrDuplicateOrError(r)) return false;\n if (r.fields === undefined) return true;\n if (isNotNullSignedResourceId(r.error)) return true;\n // Fields can be pruned away by the reader: without them nothing proves the switch. The\n // predicate must not throw, since a throw inside a tree update rebuilds the whole tree.\n const downloadable = r.fields.find((f) => f.name === \"downloadable\");\n const stream = r.fields.find((f) => f.name === \"stream\");\n if (downloadable === undefined || stream === undefined || isNullSignedResourceId(stream.value))\n return false;\n // No backend flag marks the switch: equal non-null fields mean the controller's success branch\n // has switched `stream` to the downloadable blob.\n return stream.value === downloadable.value;\n}\n\n/**\n * The finality of trees, and the default of `PlClient.finalPredicate`: a resource it calls final\n * leaves the refresh seeds, later bodies for it are ignored, and its mutable-state change sources\n * are retired. It adds Blob and StreamManager under reader assumptions stated at each entry; a\n * consumer that reads what they exclude passes its own predicate.\n */\nexport const TreeFinality: FinalityTable = CacheFinality.extend(\"tree\", [\n {\n match: { name: ResourceTypeName.Blob },\n rule: \"always\",\n why:\n \"the incarnation field is replaced later; the project tree prunes Blob's fields, and the \" +\n \"drivers read a blob by id, never through its fields\",\n requiresPruning: { type: ResourceTypeName.Blob, fields: \"all\" },\n },\n {\n match: { name: ResourceTypeName.StreamManager },\n rule: { custom: streamSwitchedOrErrored },\n why:\n \"after an error `stream` is reset in a later transaction; default traversal raises the \" +\n \"error before exposing the fields, so only an `ignoreError` read sees `stream`\",\n stopRule: {\n approx: \"readyOrDuplicateOrError\",\n reason: \"a stop rule cannot compare two fields, so it stops before `stream` switches\",\n },\n },\n]);\n\n/** @deprecated use {@link TreeFinality} */\nexport const DefaultFinalResourceDataPredicate: FinalResourceDataPredicate =\n TreeFinality.predicate();\n"],"mappings":"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"}
@@ -0,0 +1,84 @@
1
+ const require_types = require("./types.cjs");
2
+ //#region src/core/finality.ts
3
+ /** Ready for calculation, a duplicate of an original, or carrying a resource error. */
4
+ function readyOrDuplicateOrError(r) {
5
+ return r.resourceReady || require_types.isNotNullSignedResourceId(r.originalResourceId) || require_types.isNotNullSignedResourceId(r.error);
6
+ }
7
+ /** {@link readyOrDuplicateOrError}, outputs locked, and every supplied field holding a final
8
+ * non-null value or an error. Without fields the answer is about the basic part only. */
9
+ function readyAndAllOutputsFilled(r) {
10
+ if (!readyOrDuplicateOrError(r)) return false;
11
+ if (!r.outputsLocked) return false;
12
+ if (r.fields === void 0) return true;
13
+ for (const f of r.fields) if (require_types.isNullSignedResourceId(f.error) && (require_types.isNullSignedResourceId(f.value) || !f.valueIsFinal)) return false;
14
+ return true;
15
+ }
16
+ function matches(match, typeName) {
17
+ return "name" in match ? typeName === match.name : typeName.startsWith(match.prefix);
18
+ }
19
+ function matchKey(match) {
20
+ return "name" in match ? `name:${match.name}` : `prefix:${match.prefix}`;
21
+ }
22
+ function holds(rule, r) {
23
+ switch (rule) {
24
+ case "always": return true;
25
+ case "never": return false;
26
+ case "readyOrDuplicateOrError": return readyOrDuplicateOrError(r);
27
+ case "readyAndAllOutputsFilled": return readyAndAllOutputsFilled(r);
28
+ default: return rule.custom(r);
29
+ }
30
+ }
31
+ const unknownResourceTypeNames = /* @__PURE__ */ new Set();
32
+ /**
33
+ * A layer of the finality table. Each layer is its parent plus its own entries: a resource is
34
+ * final in a layer if it is final in the parent or by an entry of the layer, so a layer never
35
+ * calls final less than its parent does. A type no entry of any layer covers is never final.
36
+ */
37
+ var FinalityTable = class FinalityTable {
38
+ name;
39
+ /** The entries of this layer and its ancestors, the base first. */
40
+ entries;
41
+ constructor(name, own, parent) {
42
+ this.name = name;
43
+ const seen = /* @__PURE__ */ new Set();
44
+ for (const e of own) {
45
+ const key = matchKey(e.match);
46
+ if (seen.has(key)) throw new Error(`finality table ${name}: ${key} is listed twice`);
47
+ seen.add(key);
48
+ }
49
+ this.entries = parent === void 0 ? own : [...parent.entries, ...own];
50
+ }
51
+ /** A base layer. Throws if `entries` lists a match twice. */
52
+ static of(name, entries) {
53
+ return new FinalityTable(name, entries, void 0);
54
+ }
55
+ /** A child layer: this one plus `additions`. Throws if `additions` lists a match twice. */
56
+ extend(name, additions) {
57
+ return new FinalityTable(name, additions, this);
58
+ }
59
+ /** True when any entry of the layer or its ancestors matching `r`'s type holds for `r`. A type
60
+ * no entry matches is false, and its name is logged once. */
61
+ isFinal(r) {
62
+ let covered = false;
63
+ for (const e of this.entries) {
64
+ if (!matches(e.match, r.type.name)) continue;
65
+ covered = true;
66
+ if (holds(e.rule, r)) return true;
67
+ }
68
+ if (!covered && !unknownResourceTypeNames.has(r.type.name)) {
69
+ unknownResourceTypeNames.add(r.type.name);
70
+ console.log("UNKNOWN RESOURCE TYPE: " + r.type.name);
71
+ }
72
+ return false;
73
+ }
74
+ /** {@link isFinal} as a standalone function. */
75
+ predicate() {
76
+ return (r) => this.isFinal(r);
77
+ }
78
+ };
79
+ //#endregion
80
+ exports.FinalityTable = FinalityTable;
81
+ exports.readyAndAllOutputsFilled = readyAndAllOutputsFilled;
82
+ exports.readyOrDuplicateOrError = readyOrDuplicateOrError;
83
+
84
+ //# sourceMappingURL=finality.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"finality.cjs","names":["isNotNullSignedResourceId","isNullSignedResourceId"],"sources":["../../src/core/finality.ts"],"sourcesContent":["import type { Optional } from \"utility-types\";\nimport type { BasicResourceData, ResourceData } from \"./types\";\nimport { isNotNullSignedResourceId, isNullSignedResourceId } from \"./types\";\n\n/**\n * Tells whether a resource state is final for a consumer: whether it will not change in any way\n * that consumer observes, under the write exceptions the predicate documents. If the data carries\n * no fields (`fields` undefined), the answer is about the basic part of the resource data only.\n */\nexport type FinalResourceDataPredicate = (\n resourceData: Optional<ResourceData, \"fields\">,\n) => boolean;\n\n/** Ready for calculation, a duplicate of an original, or carrying a resource error. */\nexport function readyOrDuplicateOrError(r: ResourceData | BasicResourceData): boolean {\n return (\n r.resourceReady ||\n isNotNullSignedResourceId(r.originalResourceId) ||\n isNotNullSignedResourceId(r.error)\n );\n}\n\n/** {@link readyOrDuplicateOrError}, outputs locked, and every supplied field holding a final\n * non-null value or an error. Without fields the answer is about the basic part only. */\nexport function readyAndAllOutputsFilled(r: Optional<ResourceData, \"fields\">): boolean {\n if (!readyOrDuplicateOrError(r)) return false;\n if (!r.outputsLocked) return false;\n if (r.fields === undefined) return true;\n for (const f of r.fields)\n if (isNullSignedResourceId(f.error) && (isNullSignedResourceId(f.value) || !f.valueIsFinal))\n return false;\n return true;\n}\n\n/** A condition with a traversal stop-rule translation. */\nexport type TranslatableFinalityRule =\n | \"always\"\n | \"readyOrDuplicateOrError\"\n | \"readyAndAllOutputsFilled\";\n\n/** When a resource of a type counts as final. */\nexport type FinalityRule =\n | TranslatableFinalityRule\n | \"never\"\n | { readonly custom: FinalResourceDataPredicate };\n\n/**\n * How an entry is expressed as a traversal stop rule: `\"exact\"` translates its rule;\n * `approx` stops on the given condition, which differs from the rule for the stated reason;\n * `none` contributes no clause for this entry.\n */\nexport type FinalityStopRule =\n | \"exact\"\n | { readonly approx: TranslatableFinalityRule; readonly reason: string }\n | { readonly none: string };\n\n/** The resource types an entry covers: one type name, or every name with a prefix. */\nexport type FinalityMatch = { readonly name: string } | { readonly prefix: string };\n\ntype EntryBase = {\n readonly match: FinalityMatch;\n /** What the backend writes after the rule holds, and why that is safe for this layer. */\n readonly why: string;\n /** The field pruning the entry relies on in trees that prune; the project tree must apply it. */\n readonly requiresPruning?: { readonly type: string; readonly fields: \"all\" | readonly string[] };\n};\n\n/** One row of a finality table: the types it matches, when they count as final, why, and how the\n * rule becomes a stop rule. A custom rule must declare a non-exact stop rule. */\nexport type FinalityEntry = EntryBase &\n (\n | {\n readonly rule: TranslatableFinalityRule | \"never\";\n readonly stopRule?: Exclude<FinalityStopRule, \"exact\"> | \"exact\";\n }\n | {\n readonly rule: { readonly custom: FinalResourceDataPredicate };\n readonly stopRule: Exclude<FinalityStopRule, \"exact\">;\n }\n );\n\nfunction matches(match: FinalityMatch, typeName: string): boolean {\n return \"name\" in match ? typeName === match.name : typeName.startsWith(match.prefix);\n}\n\nfunction matchKey(match: FinalityMatch): string {\n return \"name\" in match ? `name:${match.name}` : `prefix:${match.prefix}`;\n}\n\nfunction holds(rule: FinalityRule, r: Optional<ResourceData, \"fields\">): boolean {\n switch (rule) {\n case \"always\":\n return true;\n case \"never\":\n return false;\n case \"readyOrDuplicateOrError\":\n return readyOrDuplicateOrError(r);\n case \"readyAndAllOutputsFilled\":\n return readyAndAllOutputsFilled(r);\n default:\n return rule.custom(r);\n }\n}\n\n// Unknown type names are logged once, across every table and predicate.\nconst unknownResourceTypeNames = new Set<string>();\n\n/**\n * A layer of the finality table. Each layer is its parent plus its own entries: a resource is\n * final in a layer if it is final in the parent or by an entry of the layer, so a layer never\n * calls final less than its parent does. A type no entry of any layer covers is never final.\n */\nexport class FinalityTable {\n /** The entries of this layer and its ancestors, the base first. */\n public readonly entries: readonly FinalityEntry[];\n\n private constructor(\n /** Names the layer in messages and tests. */\n public readonly name: string,\n own: readonly FinalityEntry[],\n parent: FinalityTable | undefined,\n ) {\n const seen = new Set<string>();\n for (const e of own) {\n const key = matchKey(e.match);\n if (seen.has(key)) throw new Error(`finality table ${name}: ${key} is listed twice`);\n seen.add(key);\n }\n this.entries = parent === undefined ? own : [...parent.entries, ...own];\n }\n\n /** A base layer. Throws if `entries` lists a match twice. */\n static of(name: string, entries: readonly FinalityEntry[]): FinalityTable {\n return new FinalityTable(name, entries, undefined);\n }\n\n /** A child layer: this one plus `additions`. Throws if `additions` lists a match twice. */\n extend(name: string, additions: readonly FinalityEntry[]): FinalityTable {\n return new FinalityTable(name, additions, this);\n }\n\n /** True when any entry of the layer or its ancestors matching `r`'s type holds for `r`. A type\n * no entry matches is false, and its name is logged once. */\n isFinal(r: Optional<ResourceData, \"fields\">): boolean {\n let covered = false;\n for (const e of this.entries) {\n if (!matches(e.match, r.type.name)) continue;\n covered = true;\n if (holds(e.rule, r)) return true;\n }\n if (!covered && !unknownResourceTypeNames.has(r.type.name)) {\n unknownResourceTypeNames.add(r.type.name);\n console.log(\"UNKNOWN RESOURCE TYPE: \" + r.type.name);\n }\n return false;\n }\n\n /** {@link isFinal} as a standalone function. */\n predicate(): FinalResourceDataPredicate {\n return (r) => this.isFinal(r);\n }\n}\n"],"mappings":";;;AAcA,SAAgB,wBAAwB,GAA8C;CACpF,OACE,EAAE,iBACFA,cAAAA,0BAA0B,EAAE,kBAAkB,KAC9CA,cAAAA,0BAA0B,EAAE,KAAK;AAErC;;;AAIA,SAAgB,yBAAyB,GAA8C;CACrF,IAAI,CAAC,wBAAwB,CAAC,GAAG,OAAO;CACxC,IAAI,CAAC,EAAE,eAAe,OAAO;CAC7B,IAAI,EAAE,WAAW,KAAA,GAAW,OAAO;CACnC,KAAK,MAAM,KAAK,EAAE,QAChB,IAAIC,cAAAA,uBAAuB,EAAE,KAAK,MAAMA,cAAAA,uBAAuB,EAAE,KAAK,KAAK,CAAC,EAAE,eAC5E,OAAO;CACX,OAAO;AACT;AAiDA,SAAS,QAAQ,OAAsB,UAA2B;CAChE,OAAO,UAAU,QAAQ,aAAa,MAAM,OAAO,SAAS,WAAW,MAAM,MAAM;AACrF;AAEA,SAAS,SAAS,OAA8B;CAC9C,OAAO,UAAU,QAAQ,QAAQ,MAAM,SAAS,UAAU,MAAM;AAClE;AAEA,SAAS,MAAM,MAAoB,GAA8C;CAC/E,QAAQ,MAAR;EACE,KAAK,UACH,OAAO;EACT,KAAK,SACH,OAAO;EACT,KAAK,2BACH,OAAO,wBAAwB,CAAC;EAClC,KAAK,4BACH,OAAO,yBAAyB,CAAC;EACnC,SACE,OAAO,KAAK,OAAO,CAAC;CACxB;AACF;AAGA,MAAM,2CAA2B,IAAI,IAAY;;;;;;AAOjD,IAAa,gBAAb,MAAa,cAAc;CAMP;;CAJlB;CAEA,YAEE,MACA,KACA,QACA;EAHgB,KAAA,OAAA;EAIhB,MAAM,uBAAO,IAAI,IAAY;EAC7B,KAAK,MAAM,KAAK,KAAK;GACnB,MAAM,MAAM,SAAS,EAAE,KAAK;GAC5B,IAAI,KAAK,IAAI,GAAG,GAAG,MAAM,IAAI,MAAM,kBAAkB,KAAK,IAAI,IAAI,iBAAiB;GACnF,KAAK,IAAI,GAAG;EACd;EACA,KAAK,UAAU,WAAW,KAAA,IAAY,MAAM,CAAC,GAAG,OAAO,SAAS,GAAG,GAAG;CACxE;;CAGA,OAAO,GAAG,MAAc,SAAkD;EACxE,OAAO,IAAI,cAAc,MAAM,SAAS,KAAA,CAAS;CACnD;;CAGA,OAAO,MAAc,WAAoD;EACvE,OAAO,IAAI,cAAc,MAAM,WAAW,IAAI;CAChD;;;CAIA,QAAQ,GAA8C;EACpD,IAAI,UAAU;EACd,KAAK,MAAM,KAAK,KAAK,SAAS;GAC5B,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG;GACpC,UAAU;GACV,IAAI,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO;EAC/B;EACA,IAAI,CAAC,WAAW,CAAC,yBAAyB,IAAI,EAAE,KAAK,IAAI,GAAG;GAC1D,yBAAyB,IAAI,EAAE,KAAK,IAAI;GACxC,QAAQ,IAAI,4BAA4B,EAAE,KAAK,IAAI;EACrD;EACA,OAAO;CACT;;CAGA,YAAwC;EACtC,QAAQ,MAAM,KAAK,QAAQ,CAAC;CAC9B;AACF"}
@@ -0,0 +1,81 @@
1
+ import { BasicResourceData, ResourceData } from "./types.js";
2
+ import { Optional } from "utility-types";
3
+ //#region src/core/finality.d.ts
4
+ /**
5
+ * Tells whether a resource state is final for a consumer: whether it will not change in any way
6
+ * that consumer observes, under the write exceptions the predicate documents. If the data carries
7
+ * no fields (`fields` undefined), the answer is about the basic part of the resource data only.
8
+ */
9
+ export type FinalResourceDataPredicate = (resourceData: Optional<ResourceData, "fields">) => boolean;
10
+ /** Ready for calculation, a duplicate of an original, or carrying a resource error. */
11
+ export declare function readyOrDuplicateOrError(r: ResourceData | BasicResourceData): boolean;
12
+ /** {@link readyOrDuplicateOrError}, outputs locked, and every supplied field holding a final
13
+ * non-null value or an error. Without fields the answer is about the basic part only. */
14
+ export declare function readyAndAllOutputsFilled(r: Optional<ResourceData, "fields">): boolean;
15
+ /** A condition with a traversal stop-rule translation. */
16
+ export type TranslatableFinalityRule = "always" | "readyOrDuplicateOrError" | "readyAndAllOutputsFilled";
17
+ /** When a resource of a type counts as final. */
18
+ export type FinalityRule = TranslatableFinalityRule | "never" | {
19
+ readonly custom: FinalResourceDataPredicate;
20
+ };
21
+ /**
22
+ * How an entry is expressed as a traversal stop rule: `"exact"` translates its rule;
23
+ * `approx` stops on the given condition, which differs from the rule for the stated reason;
24
+ * `none` contributes no clause for this entry.
25
+ */
26
+ export type FinalityStopRule = "exact" | {
27
+ readonly approx: TranslatableFinalityRule;
28
+ readonly reason: string;
29
+ } | {
30
+ readonly none: string;
31
+ };
32
+ /** The resource types an entry covers: one type name, or every name with a prefix. */
33
+ export type FinalityMatch = {
34
+ readonly name: string;
35
+ } | {
36
+ readonly prefix: string;
37
+ };
38
+ type EntryBase = {
39
+ readonly match: FinalityMatch;
40
+ /** What the backend writes after the rule holds, and why that is safe for this layer. */
41
+ readonly why: string;
42
+ /** The field pruning the entry relies on in trees that prune; the project tree must apply it. */
43
+ readonly requiresPruning?: {
44
+ readonly type: string;
45
+ readonly fields: "all" | readonly string[];
46
+ };
47
+ };
48
+ /** One row of a finality table: the types it matches, when they count as final, why, and how the
49
+ * rule becomes a stop rule. A custom rule must declare a non-exact stop rule. */
50
+ export type FinalityEntry = EntryBase & ({
51
+ readonly rule: TranslatableFinalityRule | "never";
52
+ readonly stopRule?: Exclude<FinalityStopRule, "exact"> | "exact";
53
+ } | {
54
+ readonly rule: {
55
+ readonly custom: FinalResourceDataPredicate;
56
+ };
57
+ readonly stopRule: Exclude<FinalityStopRule, "exact">;
58
+ });
59
+ /**
60
+ * A layer of the finality table. Each layer is its parent plus its own entries: a resource is
61
+ * final in a layer if it is final in the parent or by an entry of the layer, so a layer never
62
+ * calls final less than its parent does. A type no entry of any layer covers is never final.
63
+ */
64
+ export declare class FinalityTable {
65
+ /** Names the layer in messages and tests. */
66
+ readonly name: string;
67
+ /** The entries of this layer and its ancestors, the base first. */
68
+ readonly entries: readonly FinalityEntry[];
69
+ private constructor();
70
+ /** A base layer. Throws if `entries` lists a match twice. */
71
+ static of(name: string, entries: readonly FinalityEntry[]): FinalityTable;
72
+ /** A child layer: this one plus `additions`. Throws if `additions` lists a match twice. */
73
+ extend(name: string, additions: readonly FinalityEntry[]): FinalityTable;
74
+ /** True when any entry of the layer or its ancestors matching `r`'s type holds for `r`. A type
75
+ * no entry matches is false, and its name is logged once. */
76
+ isFinal(r: Optional<ResourceData, "fields">): boolean;
77
+ /** {@link isFinal} as a standalone function. */
78
+ predicate(): FinalResourceDataPredicate;
79
+ }
80
+ //#endregion
81
+ //# sourceMappingURL=finality.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"finality.d.ts","names":[],"sources":["../../src/core/finality.ts"],"mappings":";;;;;;;;YASY,8BACV,cAAc,SAAS;;wBAIT,wBAAwB,GAAG,eAAe;;;wBAU1C,yBAAyB,GAAG,SAAS;;YAWzC;;YAMA,eACR;WAEW,QAAQ;;;;;;;YAOX;WAEG,QAAQ;WAAmC;;WAC3C;;;YAGH;WAA2B;;WAA4B;;KAE9D;WACM,OAAO;;WAEP;;WAEA;aAA6B;aAAuB;;;;;YAKnD,gBAAgB;WAGX,MAAM;WACN,WAAW,QAAQ;;WAGnB;aAAiB,QAAQ;;WACzB,UAAU,QAAQ;;;;;;;qBAmCtB;;WAMO;;WAJF,kBAAkB;UAE3B;;SAgBA,GAAG,cAAc,kBAAkB,kBAAkB;;EAK5D,OAAO,cAAc,oBAAoB,kBAAkB;;;EAM3D,QAAQ,GAAG,SAAS;;EAepB,aAAa"}
@@ -0,0 +1,82 @@
1
+ import { isNotNullSignedResourceId, isNullSignedResourceId } from "./types.js";
2
+ //#region src/core/finality.ts
3
+ /** Ready for calculation, a duplicate of an original, or carrying a resource error. */
4
+ function readyOrDuplicateOrError(r) {
5
+ return r.resourceReady || isNotNullSignedResourceId(r.originalResourceId) || isNotNullSignedResourceId(r.error);
6
+ }
7
+ /** {@link readyOrDuplicateOrError}, outputs locked, and every supplied field holding a final
8
+ * non-null value or an error. Without fields the answer is about the basic part only. */
9
+ function readyAndAllOutputsFilled(r) {
10
+ if (!readyOrDuplicateOrError(r)) return false;
11
+ if (!r.outputsLocked) return false;
12
+ if (r.fields === void 0) return true;
13
+ for (const f of r.fields) if (isNullSignedResourceId(f.error) && (isNullSignedResourceId(f.value) || !f.valueIsFinal)) return false;
14
+ return true;
15
+ }
16
+ function matches(match, typeName) {
17
+ return "name" in match ? typeName === match.name : typeName.startsWith(match.prefix);
18
+ }
19
+ function matchKey(match) {
20
+ return "name" in match ? `name:${match.name}` : `prefix:${match.prefix}`;
21
+ }
22
+ function holds(rule, r) {
23
+ switch (rule) {
24
+ case "always": return true;
25
+ case "never": return false;
26
+ case "readyOrDuplicateOrError": return readyOrDuplicateOrError(r);
27
+ case "readyAndAllOutputsFilled": return readyAndAllOutputsFilled(r);
28
+ default: return rule.custom(r);
29
+ }
30
+ }
31
+ const unknownResourceTypeNames = /* @__PURE__ */ new Set();
32
+ /**
33
+ * A layer of the finality table. Each layer is its parent plus its own entries: a resource is
34
+ * final in a layer if it is final in the parent or by an entry of the layer, so a layer never
35
+ * calls final less than its parent does. A type no entry of any layer covers is never final.
36
+ */
37
+ var FinalityTable = class FinalityTable {
38
+ name;
39
+ /** The entries of this layer and its ancestors, the base first. */
40
+ entries;
41
+ constructor(name, own, parent) {
42
+ this.name = name;
43
+ const seen = /* @__PURE__ */ new Set();
44
+ for (const e of own) {
45
+ const key = matchKey(e.match);
46
+ if (seen.has(key)) throw new Error(`finality table ${name}: ${key} is listed twice`);
47
+ seen.add(key);
48
+ }
49
+ this.entries = parent === void 0 ? own : [...parent.entries, ...own];
50
+ }
51
+ /** A base layer. Throws if `entries` lists a match twice. */
52
+ static of(name, entries) {
53
+ return new FinalityTable(name, entries, void 0);
54
+ }
55
+ /** A child layer: this one plus `additions`. Throws if `additions` lists a match twice. */
56
+ extend(name, additions) {
57
+ return new FinalityTable(name, additions, this);
58
+ }
59
+ /** True when any entry of the layer or its ancestors matching `r`'s type holds for `r`. A type
60
+ * no entry matches is false, and its name is logged once. */
61
+ isFinal(r) {
62
+ let covered = false;
63
+ for (const e of this.entries) {
64
+ if (!matches(e.match, r.type.name)) continue;
65
+ covered = true;
66
+ if (holds(e.rule, r)) return true;
67
+ }
68
+ if (!covered && !unknownResourceTypeNames.has(r.type.name)) {
69
+ unknownResourceTypeNames.add(r.type.name);
70
+ console.log("UNKNOWN RESOURCE TYPE: " + r.type.name);
71
+ }
72
+ return false;
73
+ }
74
+ /** {@link isFinal} as a standalone function. */
75
+ predicate() {
76
+ return (r) => this.isFinal(r);
77
+ }
78
+ };
79
+ //#endregion
80
+ export { FinalityTable, readyAndAllOutputsFilled, readyOrDuplicateOrError };
81
+
82
+ //# sourceMappingURL=finality.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"finality.js","names":[],"sources":["../../src/core/finality.ts"],"sourcesContent":["import type { Optional } from \"utility-types\";\nimport type { BasicResourceData, ResourceData } from \"./types\";\nimport { isNotNullSignedResourceId, isNullSignedResourceId } from \"./types\";\n\n/**\n * Tells whether a resource state is final for a consumer: whether it will not change in any way\n * that consumer observes, under the write exceptions the predicate documents. If the data carries\n * no fields (`fields` undefined), the answer is about the basic part of the resource data only.\n */\nexport type FinalResourceDataPredicate = (\n resourceData: Optional<ResourceData, \"fields\">,\n) => boolean;\n\n/** Ready for calculation, a duplicate of an original, or carrying a resource error. */\nexport function readyOrDuplicateOrError(r: ResourceData | BasicResourceData): boolean {\n return (\n r.resourceReady ||\n isNotNullSignedResourceId(r.originalResourceId) ||\n isNotNullSignedResourceId(r.error)\n );\n}\n\n/** {@link readyOrDuplicateOrError}, outputs locked, and every supplied field holding a final\n * non-null value or an error. Without fields the answer is about the basic part only. */\nexport function readyAndAllOutputsFilled(r: Optional<ResourceData, \"fields\">): boolean {\n if (!readyOrDuplicateOrError(r)) return false;\n if (!r.outputsLocked) return false;\n if (r.fields === undefined) return true;\n for (const f of r.fields)\n if (isNullSignedResourceId(f.error) && (isNullSignedResourceId(f.value) || !f.valueIsFinal))\n return false;\n return true;\n}\n\n/** A condition with a traversal stop-rule translation. */\nexport type TranslatableFinalityRule =\n | \"always\"\n | \"readyOrDuplicateOrError\"\n | \"readyAndAllOutputsFilled\";\n\n/** When a resource of a type counts as final. */\nexport type FinalityRule =\n | TranslatableFinalityRule\n | \"never\"\n | { readonly custom: FinalResourceDataPredicate };\n\n/**\n * How an entry is expressed as a traversal stop rule: `\"exact\"` translates its rule;\n * `approx` stops on the given condition, which differs from the rule for the stated reason;\n * `none` contributes no clause for this entry.\n */\nexport type FinalityStopRule =\n | \"exact\"\n | { readonly approx: TranslatableFinalityRule; readonly reason: string }\n | { readonly none: string };\n\n/** The resource types an entry covers: one type name, or every name with a prefix. */\nexport type FinalityMatch = { readonly name: string } | { readonly prefix: string };\n\ntype EntryBase = {\n readonly match: FinalityMatch;\n /** What the backend writes after the rule holds, and why that is safe for this layer. */\n readonly why: string;\n /** The field pruning the entry relies on in trees that prune; the project tree must apply it. */\n readonly requiresPruning?: { readonly type: string; readonly fields: \"all\" | readonly string[] };\n};\n\n/** One row of a finality table: the types it matches, when they count as final, why, and how the\n * rule becomes a stop rule. A custom rule must declare a non-exact stop rule. */\nexport type FinalityEntry = EntryBase &\n (\n | {\n readonly rule: TranslatableFinalityRule | \"never\";\n readonly stopRule?: Exclude<FinalityStopRule, \"exact\"> | \"exact\";\n }\n | {\n readonly rule: { readonly custom: FinalResourceDataPredicate };\n readonly stopRule: Exclude<FinalityStopRule, \"exact\">;\n }\n );\n\nfunction matches(match: FinalityMatch, typeName: string): boolean {\n return \"name\" in match ? typeName === match.name : typeName.startsWith(match.prefix);\n}\n\nfunction matchKey(match: FinalityMatch): string {\n return \"name\" in match ? `name:${match.name}` : `prefix:${match.prefix}`;\n}\n\nfunction holds(rule: FinalityRule, r: Optional<ResourceData, \"fields\">): boolean {\n switch (rule) {\n case \"always\":\n return true;\n case \"never\":\n return false;\n case \"readyOrDuplicateOrError\":\n return readyOrDuplicateOrError(r);\n case \"readyAndAllOutputsFilled\":\n return readyAndAllOutputsFilled(r);\n default:\n return rule.custom(r);\n }\n}\n\n// Unknown type names are logged once, across every table and predicate.\nconst unknownResourceTypeNames = new Set<string>();\n\n/**\n * A layer of the finality table. Each layer is its parent plus its own entries: a resource is\n * final in a layer if it is final in the parent or by an entry of the layer, so a layer never\n * calls final less than its parent does. A type no entry of any layer covers is never final.\n */\nexport class FinalityTable {\n /** The entries of this layer and its ancestors, the base first. */\n public readonly entries: readonly FinalityEntry[];\n\n private constructor(\n /** Names the layer in messages and tests. */\n public readonly name: string,\n own: readonly FinalityEntry[],\n parent: FinalityTable | undefined,\n ) {\n const seen = new Set<string>();\n for (const e of own) {\n const key = matchKey(e.match);\n if (seen.has(key)) throw new Error(`finality table ${name}: ${key} is listed twice`);\n seen.add(key);\n }\n this.entries = parent === undefined ? own : [...parent.entries, ...own];\n }\n\n /** A base layer. Throws if `entries` lists a match twice. */\n static of(name: string, entries: readonly FinalityEntry[]): FinalityTable {\n return new FinalityTable(name, entries, undefined);\n }\n\n /** A child layer: this one plus `additions`. Throws if `additions` lists a match twice. */\n extend(name: string, additions: readonly FinalityEntry[]): FinalityTable {\n return new FinalityTable(name, additions, this);\n }\n\n /** True when any entry of the layer or its ancestors matching `r`'s type holds for `r`. A type\n * no entry matches is false, and its name is logged once. */\n isFinal(r: Optional<ResourceData, \"fields\">): boolean {\n let covered = false;\n for (const e of this.entries) {\n if (!matches(e.match, r.type.name)) continue;\n covered = true;\n if (holds(e.rule, r)) return true;\n }\n if (!covered && !unknownResourceTypeNames.has(r.type.name)) {\n unknownResourceTypeNames.add(r.type.name);\n console.log(\"UNKNOWN RESOURCE TYPE: \" + r.type.name);\n }\n return false;\n }\n\n /** {@link isFinal} as a standalone function. */\n predicate(): FinalResourceDataPredicate {\n return (r) => this.isFinal(r);\n }\n}\n"],"mappings":";;;AAcA,SAAgB,wBAAwB,GAA8C;CACpF,OACE,EAAE,iBACF,0BAA0B,EAAE,kBAAkB,KAC9C,0BAA0B,EAAE,KAAK;AAErC;;;AAIA,SAAgB,yBAAyB,GAA8C;CACrF,IAAI,CAAC,wBAAwB,CAAC,GAAG,OAAO;CACxC,IAAI,CAAC,EAAE,eAAe,OAAO;CAC7B,IAAI,EAAE,WAAW,KAAA,GAAW,OAAO;CACnC,KAAK,MAAM,KAAK,EAAE,QAChB,IAAI,uBAAuB,EAAE,KAAK,MAAM,uBAAuB,EAAE,KAAK,KAAK,CAAC,EAAE,eAC5E,OAAO;CACX,OAAO;AACT;AAiDA,SAAS,QAAQ,OAAsB,UAA2B;CAChE,OAAO,UAAU,QAAQ,aAAa,MAAM,OAAO,SAAS,WAAW,MAAM,MAAM;AACrF;AAEA,SAAS,SAAS,OAA8B;CAC9C,OAAO,UAAU,QAAQ,QAAQ,MAAM,SAAS,UAAU,MAAM;AAClE;AAEA,SAAS,MAAM,MAAoB,GAA8C;CAC/E,QAAQ,MAAR;EACE,KAAK,UACH,OAAO;EACT,KAAK,SACH,OAAO;EACT,KAAK,2BACH,OAAO,wBAAwB,CAAC;EAClC,KAAK,4BACH,OAAO,yBAAyB,CAAC;EACnC,SACE,OAAO,KAAK,OAAO,CAAC;CACxB;AACF;AAGA,MAAM,2CAA2B,IAAI,IAAY;;;;;;AAOjD,IAAa,gBAAb,MAAa,cAAc;CAMP;;CAJlB;CAEA,YAEE,MACA,KACA,QACA;EAHgB,KAAA,OAAA;EAIhB,MAAM,uBAAO,IAAI,IAAY;EAC7B,KAAK,MAAM,KAAK,KAAK;GACnB,MAAM,MAAM,SAAS,EAAE,KAAK;GAC5B,IAAI,KAAK,IAAI,GAAG,GAAG,MAAM,IAAI,MAAM,kBAAkB,KAAK,IAAI,IAAI,iBAAiB;GACnF,KAAK,IAAI,GAAG;EACd;EACA,KAAK,UAAU,WAAW,KAAA,IAAY,MAAM,CAAC,GAAG,OAAO,SAAS,GAAG,GAAG;CACxE;;CAGA,OAAO,GAAG,MAAc,SAAkD;EACxE,OAAO,IAAI,cAAc,MAAM,SAAS,KAAA,CAAS;CACnD;;CAGA,OAAO,MAAc,WAAoD;EACvE,OAAO,IAAI,cAAc,MAAM,WAAW,IAAI;CAChD;;;CAIA,QAAQ,GAA8C;EACpD,IAAI,UAAU;EACd,KAAK,MAAM,KAAK,KAAK,SAAS;GAC5B,IAAI,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,IAAI,GAAG;GACpC,UAAU;GACV,IAAI,MAAM,EAAE,MAAM,CAAC,GAAG,OAAO;EAC/B;EACA,IAAI,CAAC,WAAW,CAAC,yBAAyB,IAAI,EAAE,KAAK,IAAI,GAAG;GAC1D,yBAAyB,IAAI,EAAE,KAAK,IAAI;GACxC,QAAQ,IAAI,4BAA4B,EAAE,KAAK,IAAI;EACrD;EACA,OAAO;CACT;;CAGA,YAAwC;EACtC,QAAQ,MAAM,KAAK,QAAQ,CAAC;CAC9B;AACF"}