@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.
- 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 -68
- 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 +200 -68
- 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 +175 -0
- package/src/core/final.ts +264 -113
- 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
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from "vitest";
|
|
2
|
+
import { ResourceTypeName, ResourceTypePrefix } from "@milaboratories/pl-model-common";
|
|
3
|
+
import {
|
|
4
|
+
CacheFinality,
|
|
5
|
+
DefaultFinalResourceDataPredicate,
|
|
6
|
+
StrictFinality,
|
|
7
|
+
TreeFinality,
|
|
8
|
+
} from "./final";
|
|
9
|
+
import { FinalityTable } from "./finality";
|
|
10
|
+
import type { FinalResourceDataPredicate } from "./finality";
|
|
11
|
+
import type { FieldData, ResourceData } from "./types";
|
|
12
|
+
import { createSignedResourceId, NullSignedResourceId } from "./types";
|
|
13
|
+
|
|
14
|
+
function resource(typeName: string, resourceReady = true): ResourceData {
|
|
15
|
+
return {
|
|
16
|
+
id: createSignedResourceId(1n),
|
|
17
|
+
originalResourceId: NullSignedResourceId,
|
|
18
|
+
kind: "Structural",
|
|
19
|
+
type: { name: typeName, version: "1" },
|
|
20
|
+
data: undefined,
|
|
21
|
+
error: NullSignedResourceId,
|
|
22
|
+
inputsLocked: resourceReady,
|
|
23
|
+
outputsLocked: resourceReady,
|
|
24
|
+
resourceReady,
|
|
25
|
+
final: false,
|
|
26
|
+
fields: [],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const base = FinalityTable.of("base", [
|
|
31
|
+
{ match: { name: "A" }, rule: "readyOrDuplicateOrError", why: "test" },
|
|
32
|
+
{ match: { name: "B" }, rule: "never", why: "test" },
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
test("a child layer is its parent plus its additions", () => {
|
|
36
|
+
const child = base.extend("child", [
|
|
37
|
+
{ match: { name: "B" }, rule: "always", why: "test" },
|
|
38
|
+
{ match: { prefix: "C/" }, rule: "always", why: "test" },
|
|
39
|
+
]);
|
|
40
|
+
expect(child.isFinal(resource("A"))).toBe(true);
|
|
41
|
+
expect(child.isFinal(resource("A", false))).toBe(false);
|
|
42
|
+
expect(child.isFinal(resource("B"))).toBe(true);
|
|
43
|
+
expect(child.isFinal(resource("C/x"))).toBe(true);
|
|
44
|
+
expect(base.isFinal(resource("B"))).toBe(false);
|
|
45
|
+
expect(base.isFinal(resource("C/x"))).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("an addition cannot make its parent's final resource non-final", () => {
|
|
49
|
+
const child = base.extend("child", [{ match: { name: "A" }, rule: "never", why: "test" }]);
|
|
50
|
+
expect(child.isFinal(resource("A"))).toBe(true);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("a type no layer covers is never final", () => {
|
|
54
|
+
expect(base.extend("child", []).isFinal(resource("Unknown"))).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("a layer lists each match once", () => {
|
|
58
|
+
expect(() =>
|
|
59
|
+
FinalityTable.of("dup", [
|
|
60
|
+
{ match: { name: "A" }, rule: "always", why: "test" },
|
|
61
|
+
{ match: { name: "A" }, rule: "never", why: "test" },
|
|
62
|
+
]),
|
|
63
|
+
).toThrow(/listed twice/);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("entries list the base first", () => {
|
|
67
|
+
const child = base.extend("child", [{ match: { prefix: "C/" }, rule: "always", why: "test" }]);
|
|
68
|
+
expect(child.entries.map((e) => ("name" in e.match ? e.match.name : e.match.prefix))).toEqual([
|
|
69
|
+
"A",
|
|
70
|
+
"B",
|
|
71
|
+
"C/",
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("the built-in tables", () => {
|
|
76
|
+
type Snapshot = Parameters<FinalResourceDataPredicate>[0];
|
|
77
|
+
const id = createSignedResourceId(101n);
|
|
78
|
+
const target = createSignedResourceId(102n);
|
|
79
|
+
const error = createSignedResourceId(103n);
|
|
80
|
+
|
|
81
|
+
function field(patch: Partial<FieldData> = {}): FieldData {
|
|
82
|
+
return {
|
|
83
|
+
name: "out",
|
|
84
|
+
type: "Output",
|
|
85
|
+
status: "Resolved",
|
|
86
|
+
value: target,
|
|
87
|
+
error: NullSignedResourceId,
|
|
88
|
+
valueIsFinal: true,
|
|
89
|
+
...patch,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resource(name: string, patch: Partial<Snapshot> = {}): Snapshot {
|
|
94
|
+
return {
|
|
95
|
+
id,
|
|
96
|
+
type: { name, version: "1" },
|
|
97
|
+
kind: "Structural",
|
|
98
|
+
data: undefined,
|
|
99
|
+
originalResourceId: NullSignedResourceId,
|
|
100
|
+
error: NullSignedResourceId,
|
|
101
|
+
inputsLocked: true,
|
|
102
|
+
outputsLocked: true,
|
|
103
|
+
resourceReady: true,
|
|
104
|
+
final: false,
|
|
105
|
+
fields: [field()],
|
|
106
|
+
...patch,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// The expected answers, listed independently of the table's entries.
|
|
111
|
+
const alwaysFinal = new Set([
|
|
112
|
+
"Blob",
|
|
113
|
+
"WorkingDirectory",
|
|
114
|
+
"json/object",
|
|
115
|
+
"json-gz/object",
|
|
116
|
+
"json/string",
|
|
117
|
+
"json/array",
|
|
118
|
+
"json/number",
|
|
119
|
+
"json/bool",
|
|
120
|
+
"json/null",
|
|
121
|
+
"json/errorTrace",
|
|
122
|
+
"BContextEnd",
|
|
123
|
+
"Frontend/FromUrl",
|
|
124
|
+
"Frontend/FromFolder",
|
|
125
|
+
"Frontend/FromLocalTgz",
|
|
126
|
+
"BObjectSpec",
|
|
127
|
+
"Null",
|
|
128
|
+
"binary",
|
|
129
|
+
]);
|
|
130
|
+
const finalWhenTerminal = new Set([
|
|
131
|
+
"Dummy",
|
|
132
|
+
"StdMap",
|
|
133
|
+
"std/map",
|
|
134
|
+
"EphStdMap",
|
|
135
|
+
"PFrame",
|
|
136
|
+
"ParquetChunk",
|
|
137
|
+
"BContext",
|
|
138
|
+
"BlockPackCustom",
|
|
139
|
+
"BinaryMap",
|
|
140
|
+
"BinaryValue",
|
|
141
|
+
"BlobMap",
|
|
142
|
+
"BResolveSingleNoResult",
|
|
143
|
+
"BQueryResult",
|
|
144
|
+
"TengoTemplate",
|
|
145
|
+
"TengoLib",
|
|
146
|
+
"SoftwareInfo",
|
|
147
|
+
]);
|
|
148
|
+
const neverFinal = new Set([
|
|
149
|
+
"UserProject",
|
|
150
|
+
"Projects",
|
|
151
|
+
"ClientRoot",
|
|
152
|
+
"SharingOutbox",
|
|
153
|
+
"SharingState",
|
|
154
|
+
"SharedEnvelope",
|
|
155
|
+
"BResolveSingle",
|
|
156
|
+
"BResolveChoice",
|
|
157
|
+
"LSProvider",
|
|
158
|
+
]);
|
|
159
|
+
function terminal(r: Snapshot): boolean {
|
|
160
|
+
return (
|
|
161
|
+
r.resourceReady ||
|
|
162
|
+
r.originalResourceId !== NullSignedResourceId ||
|
|
163
|
+
r.error !== NullSignedResourceId
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
function filled(r: Snapshot): boolean {
|
|
167
|
+
return (
|
|
168
|
+
terminal(r) &&
|
|
169
|
+
r.outputsLocked &&
|
|
170
|
+
(r.fields === undefined ||
|
|
171
|
+
r.fields.every(
|
|
172
|
+
(f) =>
|
|
173
|
+
f.error !== NullSignedResourceId ||
|
|
174
|
+
(f.value !== NullSignedResourceId && f.valueIsFinal),
|
|
175
|
+
))
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
function expectedTree(r: Snapshot): boolean {
|
|
179
|
+
const name = r.type.name;
|
|
180
|
+
if (name === "BResolveSingle" || name === "BResolveChoice" || name.startsWith("BlobCopy/"))
|
|
181
|
+
return filled(r);
|
|
182
|
+
if (name === "StreamManager") {
|
|
183
|
+
if (!terminal(r)) return false;
|
|
184
|
+
if (r.fields === undefined || r.error !== NullSignedResourceId) return true;
|
|
185
|
+
const stream = r.fields.find((f) => f.name === "stream");
|
|
186
|
+
const downloadable = r.fields.find((f) => f.name === "downloadable");
|
|
187
|
+
return (
|
|
188
|
+
stream !== undefined &&
|
|
189
|
+
downloadable !== undefined &&
|
|
190
|
+
stream.value !== NullSignedResourceId &&
|
|
191
|
+
stream.value === downloadable.value
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
if (alwaysFinal.has(name)) return true;
|
|
195
|
+
if (finalWhenTerminal.has(name)) return terminal(r);
|
|
196
|
+
if (neverFinal.has(name)) return false;
|
|
197
|
+
if (name === "json/resourceError") return r.type.version === "1";
|
|
198
|
+
if (
|
|
199
|
+
["Blob/", "LS/", "WorkingDirectory/", "StorageSpaceAllocation/"].some((p) =>
|
|
200
|
+
name.startsWith(p),
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
return true;
|
|
204
|
+
if (["BlobUpload/", "BlobIndex/"].some((p) => name.startsWith(p))) return filled(r);
|
|
205
|
+
if (["PColumnData/", "StreamWorkdir/"].some((p) => name.startsWith(p))) return terminal(r);
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const names = [
|
|
210
|
+
...new Set([
|
|
211
|
+
...Object.values(ResourceTypeName),
|
|
212
|
+
...Object.values(ResourceTypePrefix).flatMap((p) => [
|
|
213
|
+
p,
|
|
214
|
+
p + "fs",
|
|
215
|
+
p + "BlobUpload/fs",
|
|
216
|
+
p.slice(0, -1),
|
|
217
|
+
"x" + p,
|
|
218
|
+
p.replace("/", "X/"),
|
|
219
|
+
]),
|
|
220
|
+
"Blob/BlobCopy/fs",
|
|
221
|
+
"BlobUpload",
|
|
222
|
+
"BlobIndex",
|
|
223
|
+
"BlobCopy",
|
|
224
|
+
"BlobUploadX/fs",
|
|
225
|
+
"LSProvider/x",
|
|
226
|
+
"StdMap/child",
|
|
227
|
+
"std/map/child",
|
|
228
|
+
"json/resourceError/child",
|
|
229
|
+
]),
|
|
230
|
+
];
|
|
231
|
+
const fieldStates: Array<Snapshot["fields"]> = [
|
|
232
|
+
undefined,
|
|
233
|
+
[],
|
|
234
|
+
[field()],
|
|
235
|
+
[field({ valueIsFinal: false })],
|
|
236
|
+
[field({ value: NullSignedResourceId, valueIsFinal: false, status: "Empty" })],
|
|
237
|
+
[field({ value: NullSignedResourceId, valueIsFinal: false, error })],
|
|
238
|
+
[field(), field({ name: "service", type: "Service", valueIsFinal: false })],
|
|
239
|
+
[field({ name: "stream" }), field({ name: "downloadable" })],
|
|
240
|
+
[field({ name: "stream" }), field({ name: "downloadable", value: error })],
|
|
241
|
+
];
|
|
242
|
+
|
|
243
|
+
test("every built-in name and prefix, at every version and state, gets the answer of the rules it is listed with", () => {
|
|
244
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
245
|
+
const mismatches: string[] = [];
|
|
246
|
+
try {
|
|
247
|
+
for (const name of names)
|
|
248
|
+
for (const version of ["", "1", "2", "01"]) {
|
|
249
|
+
for (let bits = 0; bits < 16; bits++)
|
|
250
|
+
for (const fields of fieldStates) {
|
|
251
|
+
const r = resource(name, {
|
|
252
|
+
type: { name, version },
|
|
253
|
+
resourceReady: !!(bits & 1),
|
|
254
|
+
outputsLocked: !!(bits & 2),
|
|
255
|
+
originalResourceId: bits & 4 ? target : NullSignedResourceId,
|
|
256
|
+
error: bits & 8 ? error : NullSignedResourceId,
|
|
257
|
+
fields,
|
|
258
|
+
});
|
|
259
|
+
const tree = expectedTree(r);
|
|
260
|
+
const cache = name !== "Blob" && name !== "StreamManager" && tree;
|
|
261
|
+
if (
|
|
262
|
+
TreeFinality.isFinal(r) !== tree ||
|
|
263
|
+
DefaultFinalResourceDataPredicate(r) !== tree ||
|
|
264
|
+
CacheFinality.isFinal(r) !== cache
|
|
265
|
+
)
|
|
266
|
+
mismatches.push(
|
|
267
|
+
JSON.stringify(r, (_, v) => (typeof v === "bigint" ? String(v) : v)),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
expect(mismatches).toEqual([]);
|
|
272
|
+
} finally {
|
|
273
|
+
log.mockRestore();
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("a built-in type is never logged as unknown; an unknown one is logged once, whatever its version or layer", () => {
|
|
278
|
+
const log = vi.spyOn(console, "log").mockImplementation(() => {});
|
|
279
|
+
try {
|
|
280
|
+
for (const name of [
|
|
281
|
+
...Object.values(ResourceTypeName),
|
|
282
|
+
...Object.values(ResourceTypePrefix).map((p) => p + "probe"),
|
|
283
|
+
]) {
|
|
284
|
+
for (const layer of [StrictFinality, CacheFinality, TreeFinality])
|
|
285
|
+
layer.isFinal(resource(name));
|
|
286
|
+
}
|
|
287
|
+
expect(log).not.toHaveBeenCalled();
|
|
288
|
+
const name = "unknown-log-probe/x";
|
|
289
|
+
for (const version of ["1", "2", "1"])
|
|
290
|
+
for (const layer of [StrictFinality, CacheFinality, TreeFinality]) {
|
|
291
|
+
expect(layer.isFinal(resource(name, { type: { name, version } }))).toBe(false);
|
|
292
|
+
}
|
|
293
|
+
expect(log.mock.calls).toEqual([["UNKNOWN RESOURCE TYPE: " + name]]);
|
|
294
|
+
} finally {
|
|
295
|
+
log.mockRestore();
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import type { Optional } from "utility-types";
|
|
2
|
+
import type { BasicResourceData, ResourceData } from "./types";
|
|
3
|
+
import { isNotNullSignedResourceId, isNullSignedResourceId } from "./types";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Tells whether a resource state is final for a consumer: whether it will not change in any way
|
|
7
|
+
* that consumer observes, under the write exceptions the predicate documents. If the data carries
|
|
8
|
+
* no fields (`fields` undefined), the answer is about the basic part of the resource data only.
|
|
9
|
+
*/
|
|
10
|
+
export type FinalResourceDataPredicate = (
|
|
11
|
+
resourceData: Optional<ResourceData, "fields">,
|
|
12
|
+
) => boolean;
|
|
13
|
+
|
|
14
|
+
/** Ready for calculation, a duplicate of an original, or carrying a resource error. */
|
|
15
|
+
export function readyOrDuplicateOrError(r: ResourceData | BasicResourceData): boolean {
|
|
16
|
+
return (
|
|
17
|
+
r.resourceReady ||
|
|
18
|
+
isNotNullSignedResourceId(r.originalResourceId) ||
|
|
19
|
+
isNotNullSignedResourceId(r.error)
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** {@link readyOrDuplicateOrError}, outputs locked, and every supplied field holding a final
|
|
24
|
+
* non-null value or an error. Without fields the answer is about the basic part only. */
|
|
25
|
+
export function readyAndAllOutputsFilled(r: Optional<ResourceData, "fields">): boolean {
|
|
26
|
+
if (!readyOrDuplicateOrError(r)) return false;
|
|
27
|
+
if (!r.outputsLocked) return false;
|
|
28
|
+
if (r.fields === undefined) return true;
|
|
29
|
+
for (const f of r.fields)
|
|
30
|
+
if (isNullSignedResourceId(f.error) && (isNullSignedResourceId(f.value) || !f.valueIsFinal))
|
|
31
|
+
return false;
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A condition with a traversal stop-rule translation. */
|
|
36
|
+
export type TranslatableFinalityRule =
|
|
37
|
+
| "always"
|
|
38
|
+
| "readyOrDuplicateOrError"
|
|
39
|
+
| "readyAndAllOutputsFilled";
|
|
40
|
+
|
|
41
|
+
/** When a resource of a type counts as final. */
|
|
42
|
+
export type FinalityRule =
|
|
43
|
+
| TranslatableFinalityRule
|
|
44
|
+
| "never"
|
|
45
|
+
| { readonly custom: FinalResourceDataPredicate };
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* How an entry is expressed as a traversal stop rule: `"exact"` translates its rule;
|
|
49
|
+
* `approx` stops on the given condition, which differs from the rule for the stated reason;
|
|
50
|
+
* `none` contributes no clause for this entry.
|
|
51
|
+
*/
|
|
52
|
+
export type FinalityStopRule =
|
|
53
|
+
| "exact"
|
|
54
|
+
| { readonly approx: TranslatableFinalityRule; readonly reason: string }
|
|
55
|
+
| { readonly none: string };
|
|
56
|
+
|
|
57
|
+
/** The resource types an entry covers: one type name, or every name with a prefix. */
|
|
58
|
+
export type FinalityMatch = { readonly name: string } | { readonly prefix: string };
|
|
59
|
+
|
|
60
|
+
type EntryBase = {
|
|
61
|
+
readonly match: FinalityMatch;
|
|
62
|
+
/** What the backend writes after the rule holds, and why that is safe for this layer. */
|
|
63
|
+
readonly why: string;
|
|
64
|
+
/** The field pruning the entry relies on in trees that prune; the project tree must apply it. */
|
|
65
|
+
readonly requiresPruning?: { readonly type: string; readonly fields: "all" | readonly string[] };
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** One row of a finality table: the types it matches, when they count as final, why, and how the
|
|
69
|
+
* rule becomes a stop rule. A custom rule must declare a non-exact stop rule. */
|
|
70
|
+
export type FinalityEntry = EntryBase &
|
|
71
|
+
(
|
|
72
|
+
| {
|
|
73
|
+
readonly rule: TranslatableFinalityRule | "never";
|
|
74
|
+
readonly stopRule?: Exclude<FinalityStopRule, "exact"> | "exact";
|
|
75
|
+
}
|
|
76
|
+
| {
|
|
77
|
+
readonly rule: { readonly custom: FinalResourceDataPredicate };
|
|
78
|
+
readonly stopRule: Exclude<FinalityStopRule, "exact">;
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
function matches(match: FinalityMatch, typeName: string): boolean {
|
|
83
|
+
return "name" in match ? typeName === match.name : typeName.startsWith(match.prefix);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function matchKey(match: FinalityMatch): string {
|
|
87
|
+
return "name" in match ? `name:${match.name}` : `prefix:${match.prefix}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function holds(rule: FinalityRule, r: Optional<ResourceData, "fields">): boolean {
|
|
91
|
+
switch (rule) {
|
|
92
|
+
case "always":
|
|
93
|
+
return true;
|
|
94
|
+
case "never":
|
|
95
|
+
return false;
|
|
96
|
+
case "readyOrDuplicateOrError":
|
|
97
|
+
return readyOrDuplicateOrError(r);
|
|
98
|
+
case "readyAndAllOutputsFilled":
|
|
99
|
+
return readyAndAllOutputsFilled(r);
|
|
100
|
+
default:
|
|
101
|
+
return rule.custom(r);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Unknown type names are logged once, across every table and predicate.
|
|
106
|
+
const unknownResourceTypeNames = new Set<string>();
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A layer of the finality table. Each layer is its parent plus its own entries: a resource is
|
|
110
|
+
* final in a layer if it is final in the parent or by an entry of the layer, so a layer never
|
|
111
|
+
* calls final less than its parent does. A type no entry of any layer covers is never final.
|
|
112
|
+
*/
|
|
113
|
+
export class FinalityTable {
|
|
114
|
+
/** The entries of this layer and its ancestors, the base first. */
|
|
115
|
+
public readonly entries: readonly FinalityEntry[];
|
|
116
|
+
|
|
117
|
+
private constructor(
|
|
118
|
+
/** Names the layer in messages and tests. */
|
|
119
|
+
public readonly name: string,
|
|
120
|
+
own: readonly FinalityEntry[],
|
|
121
|
+
parent: FinalityTable | undefined,
|
|
122
|
+
) {
|
|
123
|
+
const seen = new Set<string>();
|
|
124
|
+
for (const e of own) {
|
|
125
|
+
const key = matchKey(e.match);
|
|
126
|
+
if (seen.has(key)) throw new Error(`finality table ${name}: ${key} is listed twice`);
|
|
127
|
+
seen.add(key);
|
|
128
|
+
}
|
|
129
|
+
this.entries = parent === undefined ? own : [...parent.entries, ...own];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** A base layer. Throws if `entries` lists a match twice. */
|
|
133
|
+
static of(name: string, entries: readonly FinalityEntry[]): FinalityTable {
|
|
134
|
+
return new FinalityTable(name, entries, undefined);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** A child layer: this one plus `additions`. Throws if `additions` lists a match twice. */
|
|
138
|
+
extend(name: string, additions: readonly FinalityEntry[]): FinalityTable {
|
|
139
|
+
return new FinalityTable(name, additions, this);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** True when any entry of the layer or its ancestors matching `r`'s type holds for `r`. A type
|
|
143
|
+
* no entry matches is false, and its name is logged once. */
|
|
144
|
+
isFinal(r: Optional<ResourceData, "fields">): boolean {
|
|
145
|
+
let covered = false;
|
|
146
|
+
for (const e of this.entries) {
|
|
147
|
+
if (!matches(e.match, r.type.name)) continue;
|
|
148
|
+
covered = true;
|
|
149
|
+
if (holds(e.rule, r)) return true;
|
|
150
|
+
}
|
|
151
|
+
if (!covered && !unknownResourceTypeNames.has(r.type.name)) {
|
|
152
|
+
unknownResourceTypeNames.add(r.type.name);
|
|
153
|
+
console.log("UNKNOWN RESOURCE TYPE: " + r.type.name);
|
|
154
|
+
}
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** {@link isFinal} as a standalone function. */
|
|
159
|
+
predicate(): FinalResourceDataPredicate {
|
|
160
|
+
return (r) => this.isFinal(r);
|
|
161
|
+
}
|
|
162
|
+
}
|
package/src/core/transaction.ts
CHANGED
|
@@ -278,7 +278,8 @@ export class PlTransaction {
|
|
|
278
278
|
public readonly name: string,
|
|
279
279
|
public readonly writable: boolean,
|
|
280
280
|
private readonly _clientRoot: OptionalSignedResourceId,
|
|
281
|
-
|
|
281
|
+
/** What {@link sharedResourceDataCache} may keep. */
|
|
282
|
+
private readonly cachePredicate: FinalResourceDataPredicate,
|
|
282
283
|
private readonly sharedResourceDataCache: LRUCache<SignedResourceId, ResourceDataCacheRecord>,
|
|
283
284
|
private readonly enableFormattedErrors: boolean = false,
|
|
284
285
|
) {
|
|
@@ -767,7 +768,7 @@ export class PlTransaction {
|
|
|
767
768
|
|
|
768
769
|
// we will cache only final resource data states
|
|
769
770
|
// caching result even if we were ignore the cache
|
|
770
|
-
if (isResourceId(rId) && this.
|
|
771
|
+
if (isResourceId(rId) && this.cachePredicate(result)) {
|
|
771
772
|
deepFreeze(result);
|
|
772
773
|
const fromCache = this.sharedResourceDataCache.get(rId);
|
|
773
774
|
if (fromCache) {
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ export * from "./core/unauth_client";
|
|
|
10
10
|
export * from "./core/auth";
|
|
11
11
|
export * from "./core/capabilities";
|
|
12
12
|
export * from "./core/final";
|
|
13
|
+
export * from "./core/finality";
|
|
13
14
|
export * from "./core/tree_filter";
|
|
14
15
|
export * from "./core/user_resources";
|
|
15
16
|
export * from "./core/wire";
|