@milaboratories/pl-client 3.2.5 → 3.3.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 +24 -56
- package/dist/core/client.cjs.map +1 -1
- package/dist/core/client.d.ts +12 -8
- package/dist/core/client.d.ts.map +1 -1
- package/dist/core/client.js +26 -58
- package/dist/core/client.js.map +1 -1
- package/dist/core/errors.cjs +20 -0
- package/dist/core/errors.cjs.map +1 -1
- package/dist/core/errors.d.ts +6 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +19 -1
- package/dist/core/errors.js.map +1 -1
- package/dist/core/final.cjs +6 -5
- package/dist/core/final.cjs.map +1 -1
- package/dist/core/final.d.ts.map +1 -1
- package/dist/core/final.js +7 -6
- package/dist/core/final.js.map +1 -1
- package/dist/core/ll_client.cjs +18 -1
- package/dist/core/ll_client.cjs.map +1 -1
- package/dist/core/ll_client.d.ts +6 -2
- package/dist/core/ll_client.d.ts.map +1 -1
- package/dist/core/ll_client.js +19 -2
- package/dist/core/ll_client.js.map +1 -1
- package/dist/core/transaction.cjs +109 -75
- package/dist/core/transaction.cjs.map +1 -1
- package/dist/core/transaction.d.ts +30 -22
- package/dist/core/transaction.d.ts.map +1 -1
- package/dist/core/transaction.js +111 -76
- package/dist/core/transaction.js.map +1 -1
- package/dist/core/type_conversion.cjs +14 -6
- package/dist/core/type_conversion.cjs.map +1 -1
- package/dist/core/type_conversion.js +14 -6
- package/dist/core/type_conversion.js.map +1 -1
- package/dist/core/types.cjs +77 -17
- package/dist/core/types.cjs.map +1 -1
- package/dist/core/types.d.ts +49 -26
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +66 -14
- package/dist/core/types.js.map +1 -1
- package/dist/core/user_resources.cjs +181 -0
- package/dist/core/user_resources.cjs.map +1 -0
- package/dist/core/user_resources.d.ts +75 -0
- package/dist/core/user_resources.d.ts.map +1 -0
- package/dist/core/user_resources.js +180 -0
- package/dist/core/user_resources.js.map +1 -0
- package/dist/helpers/poll.cjs +4 -4
- package/dist/helpers/poll.cjs.map +1 -1
- package/dist/helpers/poll.d.ts +3 -3
- package/dist/helpers/poll.d.ts.map +1 -1
- package/dist/helpers/poll.js +5 -5
- package/dist/helpers/poll.js.map +1 -1
- package/dist/helpers/tx_helpers.cjs +1 -1
- package/dist/helpers/tx_helpers.cjs.map +1 -1
- package/dist/helpers/tx_helpers.d.ts +3 -3
- package/dist/helpers/tx_helpers.d.ts.map +1 -1
- package/dist/helpers/tx_helpers.js +2 -2
- package/dist/helpers/tx_helpers.js.map +1 -1
- package/dist/index.cjs +16 -5
- package/dist/index.d.ts +5 -4
- package/dist/index.js +5 -4
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.cjs +724 -188
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.cjs.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.cjs +34 -9
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.cjs.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.d.ts +37 -5
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.d.ts.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.js +34 -9
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.js.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.d.ts +326 -136
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.d.ts.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.js +724 -188
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.js.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.cjs +18 -7
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.cjs.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.d.ts +11 -7
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.d.ts.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.js +18 -7
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.js.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.cjs +57 -2
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.cjs.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.d.ts +26 -3
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.d.ts.map +1 -1
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.js +57 -3
- package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.js.map +1 -1
- package/dist/proto-rest/plapi.d.ts +421 -207
- package/dist/proto-rest/plapi.d.ts.map +1 -1
- package/dist/test/test_config.cjs +6 -3
- package/dist/test/test_config.cjs.map +1 -1
- package/dist/test/test_config.d.ts.map +1 -1
- package/dist/test/test_config.js +7 -4
- package/dist/test/test_config.js.map +1 -1
- package/package.json +5 -5
- package/src/core/client.ts +58 -103
- package/src/core/errors.ts +23 -0
- package/src/core/final.ts +16 -6
- package/src/core/ll_client.ts +39 -3
- package/src/core/ll_transaction.test.ts +41 -6
- package/src/core/transaction.ts +176 -86
- package/src/core/type_conversion.ts +24 -9
- package/src/core/types.ts +147 -41
- package/src/core/user_resources.ts +332 -0
- package/src/helpers/poll.ts +15 -8
- package/src/helpers/state_helpers.ts +2 -2
- package/src/helpers/tx_helpers.ts +5 -5
- package/src/index.ts +1 -0
- package/src/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.ts +61 -14
- package/src/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.ts +1045 -379
- package/src/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.ts +33 -18
- package/src/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.ts +75 -6
- package/src/proto-grpc/google/protobuf/descriptor.ts +5 -2
- package/src/proto-rest/plapi.ts +447 -225
- package/src/test/test_config.ts +8 -5
package/src/core/types.ts
CHANGED
|
@@ -1,42 +1,27 @@
|
|
|
1
|
+
import type { Branded } from "@milaboratories/pl-model-common";
|
|
1
2
|
import { cachedDeserialize, notEmpty } from "@milaboratories/ts-helpers";
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
type BrandResourceId<B> = bigint & { [__resource_id_type__]: B };
|
|
4
|
+
/** Null resource id */
|
|
5
|
+
export type NullResourceId = Branded<bigint, "null", "__resource_id__">;
|
|
6
6
|
|
|
7
7
|
/** Global resource id */
|
|
8
|
-
export type
|
|
9
|
-
|
|
10
|
-
/** Null resource id */
|
|
11
|
-
export type NullResourceId = BrandResourceId<"null">;
|
|
8
|
+
export type GlobalResourceId = Branded<bigint, "global", "__resource_id__">;
|
|
12
9
|
|
|
13
10
|
/** Local resource id */
|
|
14
|
-
export type LocalResourceId =
|
|
11
|
+
export type LocalResourceId = Branded<bigint, "local", "__resource_id__">;
|
|
15
12
|
|
|
16
13
|
/** Any non-null resource id */
|
|
17
|
-
export type AnyResourceId =
|
|
18
|
-
|
|
19
|
-
/** Any resource id */
|
|
20
|
-
export type OptionalResourceId = NullResourceId | ResourceId;
|
|
14
|
+
export type AnyResourceId = GlobalResourceId | LocalResourceId;
|
|
21
15
|
|
|
22
16
|
/** All possible resource flavours */
|
|
23
|
-
export type OptionalAnyResourceId = NullResourceId |
|
|
17
|
+
export type OptionalAnyResourceId = NullResourceId | GlobalResourceId | LocalResourceId;
|
|
24
18
|
|
|
25
19
|
export const NullResourceId = 0n as NullResourceId;
|
|
26
20
|
|
|
27
|
-
|
|
21
|
+
function isNullResourceId(resourceId: bigint | string): resourceId is NullResourceId {
|
|
28
22
|
return resourceId === NullResourceId;
|
|
29
23
|
}
|
|
30
24
|
|
|
31
|
-
export function isNotNullResourceId(resourceId: OptionalResourceId): resourceId is ResourceId {
|
|
32
|
-
return resourceId !== NullResourceId;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function ensureResourceIdNotNull(resourceId: OptionalResourceId): ResourceId {
|
|
36
|
-
if (!isNotNullResourceId(resourceId)) throw new Error("null resource id");
|
|
37
|
-
return resourceId;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
25
|
export function isAnyResourceId(resourceId: bigint): resourceId is AnyResourceId {
|
|
41
26
|
return resourceId !== 0n;
|
|
42
27
|
}
|
|
@@ -64,21 +49,29 @@ export function resourceTypeToString(rt: ResourceType): string {
|
|
|
64
49
|
return `${rt.name}:${rt.version}`;
|
|
65
50
|
}
|
|
66
51
|
|
|
52
|
+
export function parseResourceType(str: string): ResourceType {
|
|
53
|
+
const [name, version] = str.split(":");
|
|
54
|
+
return { name, version };
|
|
55
|
+
}
|
|
56
|
+
|
|
67
57
|
export function resourceTypesEqual(type1: ResourceType, type2: ResourceType): boolean {
|
|
68
58
|
return type1.name === type2.name && type1.version === type2.version;
|
|
69
59
|
}
|
|
70
60
|
|
|
61
|
+
/** Color proof used for resource creation requests (alias for ResourceSignature). */
|
|
62
|
+
export type ColorProof = ResourceSignature;
|
|
63
|
+
|
|
71
64
|
/** Readonly fields here marks properties of resource that can't change according to pl's state machine. */
|
|
72
65
|
export type BasicResourceData = {
|
|
73
|
-
readonly id:
|
|
74
|
-
readonly originalResourceId:
|
|
66
|
+
readonly id: SignedResourceId;
|
|
67
|
+
readonly originalResourceId: OptionalSignedResourceId;
|
|
75
68
|
|
|
76
69
|
readonly kind: ResourceKind;
|
|
77
70
|
readonly type: ResourceType;
|
|
78
71
|
|
|
79
72
|
readonly data?: Uint8Array;
|
|
80
73
|
|
|
81
|
-
readonly error:
|
|
74
|
+
readonly error: OptionalSignedResourceId;
|
|
82
75
|
|
|
83
76
|
readonly inputsLocked: boolean;
|
|
84
77
|
readonly outputsLocked: boolean;
|
|
@@ -132,8 +125,8 @@ export type FieldData = {
|
|
|
132
125
|
readonly name: string;
|
|
133
126
|
readonly type: FieldType;
|
|
134
127
|
readonly status: FieldStatus;
|
|
135
|
-
readonly value:
|
|
136
|
-
readonly error:
|
|
128
|
+
readonly value: OptionalSignedResourceId;
|
|
129
|
+
readonly error: OptionalSignedResourceId;
|
|
137
130
|
|
|
138
131
|
/** True if value the fields points to is in final state. */
|
|
139
132
|
readonly valueIsFinal: boolean;
|
|
@@ -164,7 +157,11 @@ export function isRootResourceId(id: bigint) {
|
|
|
164
157
|
return (id & ResourceIdRootMask) !== 0n;
|
|
165
158
|
}
|
|
166
159
|
|
|
167
|
-
export function isLocalResourceId(id: bigint): id is LocalResourceId {
|
|
160
|
+
export function isLocalResourceId(id: bigint | string): id is LocalResourceId {
|
|
161
|
+
if (typeof id === "string") {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
168
165
|
return (id & ResourceIdLocalMask) !== 0n;
|
|
169
166
|
}
|
|
170
167
|
|
|
@@ -186,8 +183,8 @@ export function createLocalResourceId(
|
|
|
186
183
|
(BigInt(localTxId) << LocalResourceIdTxIdOffset)) as LocalResourceId;
|
|
187
184
|
}
|
|
188
185
|
|
|
189
|
-
export function createGlobalResourceId(isRoot: boolean, unmaskedId: bigint):
|
|
190
|
-
return ((isRoot ? ResourceIdRootMask : 0n) | unmaskedId) as
|
|
186
|
+
export function createGlobalResourceId(isRoot: boolean, unmaskedId: bigint): GlobalResourceId {
|
|
187
|
+
return ((isRoot ? ResourceIdRootMask : 0n) | unmaskedId) as GlobalResourceId;
|
|
191
188
|
}
|
|
192
189
|
|
|
193
190
|
export function extractTxId(localResourceId: LocalResourceId): number {
|
|
@@ -202,8 +199,17 @@ export function checkLocalityOfResourceId(resourceId: AnyResourceId, expectedTxI
|
|
|
202
199
|
);
|
|
203
200
|
}
|
|
204
201
|
|
|
205
|
-
export function resourceIdToString(
|
|
202
|
+
export function resourceIdToString(
|
|
203
|
+
resourceId: OptionalAnyResourceId | OptionalSignedResourceId,
|
|
204
|
+
): string {
|
|
205
|
+
if (isSignedResourceId(resourceId)) {
|
|
206
|
+
// Strip signature
|
|
207
|
+
resourceId = anyResourceIdToBigint(resourceId) as GlobalResourceId;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (isNullSignedResourceId(resourceId)) return "XX:0x0";
|
|
206
211
|
if (isNullResourceId(resourceId)) return "XX:0x0";
|
|
212
|
+
|
|
207
213
|
if (isLocalResourceId(resourceId))
|
|
208
214
|
return (
|
|
209
215
|
(isRootResourceId(resourceId) ? "R" : "N") +
|
|
@@ -234,16 +240,116 @@ export function resourceIdFromString(str: string): OptionalAnyResourceId | undef
|
|
|
234
240
|
else return createGlobalResourceId(rn === "R", BigInt("0x" + rid));
|
|
235
241
|
}
|
|
236
242
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
+
export function anyResourceIdToBigint(resourceId: bigint | SignedResourceId): bigint {
|
|
244
|
+
if (typeof resourceId !== "string") {
|
|
245
|
+
return resourceId;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const parsed = parseSignedResourceId(resourceId);
|
|
249
|
+
return parsed.globalId as bigint;
|
|
243
250
|
}
|
|
244
251
|
|
|
245
252
|
export function stringifyWithResourceId(object: unknown): string {
|
|
246
|
-
return JSON.stringify(object, (key, value) =>
|
|
247
|
-
typeof value === "bigint"
|
|
248
|
-
|
|
253
|
+
return JSON.stringify(object, (key, value) => {
|
|
254
|
+
if (typeof value === "bigint") return resourceIdToString(value as OptionalAnyResourceId);
|
|
255
|
+
if (isSignedResourceId(value)) return resourceIdToString(value);
|
|
256
|
+
return value;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Opaque authorization signature attached to a resource. */
|
|
261
|
+
export type ResourceSignature = Branded<Uint8Array, "ResourceSignature">;
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Signed resource id is "<global ID>|<resource signature hex>", encoded as string
|
|
265
|
+
* (e.g. "NG:0x123EC|1234567890abcdef")
|
|
266
|
+
*/
|
|
267
|
+
export type SignedResourceId = Branded<string, "signed", "__signed_resource_id__">;
|
|
268
|
+
|
|
269
|
+
export type NullSignedResourceId = Branded<string, "null", "__signed_resource_id__">;
|
|
270
|
+
|
|
271
|
+
export const NullSignedResourceId = "" as NullSignedResourceId;
|
|
272
|
+
|
|
273
|
+
/** Nullable signed resource ID */
|
|
274
|
+
export type OptionalSignedResourceId = NullSignedResourceId | SignedResourceId;
|
|
275
|
+
|
|
276
|
+
export function isNullSignedResourceId(
|
|
277
|
+
resourceId: bigint | string,
|
|
278
|
+
): resourceId is NullSignedResourceId {
|
|
279
|
+
return resourceId === NullSignedResourceId;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
export function isNotNullSignedResourceId(
|
|
283
|
+
resourceId: OptionalSignedResourceId,
|
|
284
|
+
): resourceId is SignedResourceId {
|
|
285
|
+
// lint-allow-cast
|
|
286
|
+
return resourceId !== NullSignedResourceId;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export function ensureSignedResourceIdNotNull(
|
|
290
|
+
resourceId: OptionalSignedResourceId,
|
|
291
|
+
): SignedResourceId {
|
|
292
|
+
if (!isNotNullSignedResourceId(resourceId)) throw new Error("null resource id");
|
|
293
|
+
return resourceId;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function isSignedResourceId(resourceId: bigint | string): resourceId is SignedResourceId {
|
|
297
|
+
// lint-allow-cast
|
|
298
|
+
return typeof resourceId === "string" && resourceId.includes("|");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Encode resource signature to base64url for embedding in URL-based handles. */
|
|
302
|
+
export function signatureToBase64Url(sig?: ResourceSignature): string {
|
|
303
|
+
return sig && sig.length > 0 ? Buffer.from(sig).toString("base64url") : "";
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Cast raw bytes to a branded ResourceSignature, returning undefined for empty/missing input. */
|
|
307
|
+
export function toResourceSignature(raw?: Uint8Array): ResourceSignature {
|
|
308
|
+
return raw && raw.length > 0
|
|
309
|
+
? (raw as ResourceSignature)
|
|
310
|
+
: (new Uint8Array(0) as ResourceSignature);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Decode base64url-encoded string back to a branded ResourceSignature. */
|
|
314
|
+
export function base64UrlToSignature(str: string): ResourceSignature {
|
|
315
|
+
return toResourceSignature(Buffer.from(str, "base64url"))!;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Converts bigint global resource id and signature to a SignedResourceId string.
|
|
319
|
+
* Format: "<globalIdString>|<signatureHex>" */
|
|
320
|
+
export function createSignedResourceId(
|
|
321
|
+
globalId: bigint,
|
|
322
|
+
signature?: ResourceSignature,
|
|
323
|
+
): SignedResourceId {
|
|
324
|
+
if (isLocalResourceId(globalId))
|
|
325
|
+
throw new Error(`Local resource id: ${resourceIdToString(globalId)}`);
|
|
326
|
+
if (isNullResourceId(globalId)) throw new Error(`Null resource id.`);
|
|
327
|
+
|
|
328
|
+
const sigHex = signature ? Buffer.from(signature).toString("hex") : "";
|
|
329
|
+
return `${String(globalId)}|${sigHex}` as SignedResourceId; // lint-allow-cast
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function parseSignedResourceId(resourceId: SignedResourceId): {
|
|
333
|
+
globalId: GlobalResourceId;
|
|
334
|
+
signature: ResourceSignature;
|
|
335
|
+
} {
|
|
336
|
+
if (typeof resourceId !== "string") {
|
|
337
|
+
throw new Error(`Not a signed resource id: ${resourceId}`);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const pipeIdx = resourceId.indexOf("|");
|
|
341
|
+
if (pipeIdx < 0) throw new Error(`Malformed signed resource id (no '|'): ${resourceId}`);
|
|
342
|
+
|
|
343
|
+
const globalIdStr = resourceId.substring(0, pipeIdx);
|
|
344
|
+
const signatureHex = resourceId.substring(pipeIdx + 1);
|
|
345
|
+
|
|
346
|
+
const globalId = BigInt(globalIdStr);
|
|
347
|
+
if (isNullSignedResourceId(globalId) || isLocalResourceId(globalId))
|
|
348
|
+
throw new Error(`Invalid global id portion in signed resource id: ${globalIdStr}`);
|
|
349
|
+
|
|
350
|
+
const signature: ResourceSignature = (
|
|
351
|
+
signatureHex.length > 0 ? Buffer.from(signatureHex, "hex") : new Uint8Array(0)
|
|
352
|
+
) as ResourceSignature;
|
|
353
|
+
|
|
354
|
+
return { globalId: globalId as GlobalResourceId, signature };
|
|
249
355
|
}
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { LLPlClient } from "./ll_client";
|
|
3
|
+
import type { PlTransaction } from "./transaction";
|
|
4
|
+
import { toGlobalResourceId } from "./transaction";
|
|
5
|
+
import type { OptionalSignedResourceId, SignedResourceId, ResourceType } from "./types";
|
|
6
|
+
import {
|
|
7
|
+
createSignedResourceId,
|
|
8
|
+
isNotNullSignedResourceId,
|
|
9
|
+
NullSignedResourceId,
|
|
10
|
+
toResourceSignature,
|
|
11
|
+
} from "./types";
|
|
12
|
+
import { isUnimplementedError } from "./errors";
|
|
13
|
+
import { ClientRoot } from "../helpers/pl";
|
|
14
|
+
|
|
15
|
+
const AnonymousClientRoot = "AnonymousRoot";
|
|
16
|
+
const LsStorageTypePrefix = "LS/"; // implements ls API in particular storage
|
|
17
|
+
const LsProviderFieldPrefix = "storage/"; // provides access to storages list
|
|
18
|
+
|
|
19
|
+
/** Information about a single data library (LS storage). */
|
|
20
|
+
export interface StorageInfo {
|
|
21
|
+
/** Machine-stable identifier, e.g. "library". Used for filtering and map keys. */
|
|
22
|
+
readonly storageId: string;
|
|
23
|
+
/** Human-readable display name. For V1/legacy equals storageId; for V2 from resource JSON data. */
|
|
24
|
+
readonly storageName: string;
|
|
25
|
+
/** Signed resource ID for this storage resource. */
|
|
26
|
+
readonly resourceId: SignedResourceId;
|
|
27
|
+
/** Full resource type including correct version ("1" or "2"). */
|
|
28
|
+
readonly resourceType: ResourceType;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** V2 LsStorage resource JSON data shape (contract with backend). */
|
|
32
|
+
interface LsStorageV2Data {
|
|
33
|
+
storageName: string;
|
|
34
|
+
storageID: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Callback type for running transactions. Matches PlClient._withTx signature
|
|
39
|
+
* so the index can run transactions before PlClient is fully initialized.
|
|
40
|
+
*/
|
|
41
|
+
export type TxRunner = <T>(
|
|
42
|
+
name: string,
|
|
43
|
+
writable: boolean,
|
|
44
|
+
clientRoot: OptionalSignedResourceId,
|
|
45
|
+
body: (tx: PlTransaction) => Promise<T>,
|
|
46
|
+
) => Promise<T>;
|
|
47
|
+
|
|
48
|
+
type BackendCapability = "getUserRoot" | "listUserResources" | "legacy";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Abstracts user resource discovery with backward compatibility.
|
|
52
|
+
*
|
|
53
|
+
* Detects backend capability on the first getUserRoot() call and remembers
|
|
54
|
+
* the result. Three-tier fallback:
|
|
55
|
+
* 1. getUserRoot RPC (newest, supports doNotCreate)
|
|
56
|
+
* 2. listUserResources RPC (streams all resources, picks userRoot)
|
|
57
|
+
* 3. Named resource lookup/creation via transaction (legacy)
|
|
58
|
+
*/
|
|
59
|
+
export class UserResources {
|
|
60
|
+
private backendCapability: BackendCapability | undefined;
|
|
61
|
+
|
|
62
|
+
constructor(
|
|
63
|
+
private readonly ll: LLPlClient,
|
|
64
|
+
private readonly runTx: TxRunner,
|
|
65
|
+
public readonly authUser: string | null,
|
|
66
|
+
) {}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Returns the user's root resource ID.
|
|
70
|
+
*
|
|
71
|
+
* On first call, detects backend capability by trying methods in order:
|
|
72
|
+
* 1. getUserRoot RPC (newest)
|
|
73
|
+
* 2. listUserResources RPC
|
|
74
|
+
* 3. Named resource lookup/creation via transaction (legacy)
|
|
75
|
+
*/
|
|
76
|
+
async getUserRoot(): Promise<SignedResourceId>;
|
|
77
|
+
async getUserRoot(opts: { login?: string }): Promise<SignedResourceId>;
|
|
78
|
+
async getUserRoot(opts: { login?: string; doNotCreate: false }): Promise<SignedResourceId>;
|
|
79
|
+
async getUserRoot(opts: {
|
|
80
|
+
login?: string;
|
|
81
|
+
doNotCreate: true;
|
|
82
|
+
}): Promise<SignedResourceId | undefined>;
|
|
83
|
+
async getUserRoot(
|
|
84
|
+
opts: { login?: string; doNotCreate?: boolean } = {},
|
|
85
|
+
): Promise<SignedResourceId | undefined> {
|
|
86
|
+
if (this.backendCapability === undefined) {
|
|
87
|
+
return await this.detectAndGetUserRoot(opts);
|
|
88
|
+
}
|
|
89
|
+
return await this.getUserRootWith(this.backendCapability, opts);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private async detectAndGetUserRoot(
|
|
93
|
+
opts: { login?: string; doNotCreate?: boolean } = {},
|
|
94
|
+
): Promise<SignedResourceId | undefined> {
|
|
95
|
+
// 1. Try getUserRoot RPC
|
|
96
|
+
try {
|
|
97
|
+
const root = await this.getUserRootViaRpc(opts);
|
|
98
|
+
this.backendCapability = "getUserRoot";
|
|
99
|
+
return root;
|
|
100
|
+
} catch (err) {
|
|
101
|
+
if (!isUnimplementedError(err)) throw err;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 2. Try listUserResources
|
|
105
|
+
try {
|
|
106
|
+
const root = await this.getUserRootViaList(opts);
|
|
107
|
+
this.backendCapability = "listUserResources";
|
|
108
|
+
return root;
|
|
109
|
+
} catch (err) {
|
|
110
|
+
if (!isUnimplementedError(err)) throw err;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 3. Legacy fallback
|
|
114
|
+
this.backendCapability = "legacy";
|
|
115
|
+
return await this.getUserRootViaLegacy(opts);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private async getUserRootWith(
|
|
119
|
+
capability: BackendCapability,
|
|
120
|
+
opts: { login?: string; doNotCreate?: boolean } = {},
|
|
121
|
+
): Promise<SignedResourceId | undefined> {
|
|
122
|
+
switch (capability) {
|
|
123
|
+
case "getUserRoot":
|
|
124
|
+
return await this.getUserRootViaRpc(opts);
|
|
125
|
+
case "listUserResources":
|
|
126
|
+
return await this.getUserRootViaList(opts);
|
|
127
|
+
case "legacy":
|
|
128
|
+
return await this.getUserRootViaLegacy(opts);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Returns all data libraries the user has access to.
|
|
134
|
+
* Always fetches fresh from the server (no caching).
|
|
135
|
+
*/
|
|
136
|
+
async getDataLibraries(
|
|
137
|
+
opts: { login?: string; doNotCreateUserRoot?: boolean } = {},
|
|
138
|
+
): Promise<ReadonlyMap<string, StorageInfo>> {
|
|
139
|
+
if (this.backendCapability === undefined) {
|
|
140
|
+
// First call — detect backend capability
|
|
141
|
+
try {
|
|
142
|
+
const libs = await this.getDataLibrariesViaList(opts);
|
|
143
|
+
// getUserRoot RPC doesn't return libraries, but listUserResources does;
|
|
144
|
+
// record at least "listUserResources" so future getUserRoot calls don't re-detect.
|
|
145
|
+
this.backendCapability = "listUserResources";
|
|
146
|
+
return libs;
|
|
147
|
+
} catch (err) {
|
|
148
|
+
if (!isUnimplementedError(err)) throw err;
|
|
149
|
+
this.backendCapability = "legacy";
|
|
150
|
+
return await this.getDataLibrariesViaLegacy();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// A server that supports getUserRoot definitely supports listUserResources.
|
|
155
|
+
if (this.backendCapability !== "legacy") {
|
|
156
|
+
return await this.getDataLibrariesViaList(opts);
|
|
157
|
+
}
|
|
158
|
+
return await this.getDataLibrariesViaLegacy();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private async getUserRootViaRpc(opts: { login?: string }): Promise<SignedResourceId>;
|
|
162
|
+
private async getUserRootViaRpc(opts: {
|
|
163
|
+
login?: string;
|
|
164
|
+
doNotCreate: false;
|
|
165
|
+
}): Promise<SignedResourceId>;
|
|
166
|
+
private async getUserRootViaRpc(opts: {
|
|
167
|
+
login?: string;
|
|
168
|
+
doNotCreate: true;
|
|
169
|
+
}): Promise<SignedResourceId | undefined>;
|
|
170
|
+
private async getUserRootViaRpc(
|
|
171
|
+
opts: { login?: string; doNotCreate?: boolean } = {},
|
|
172
|
+
): Promise<SignedResourceId | undefined> {
|
|
173
|
+
const resp = await this.ll.getUserRoot({
|
|
174
|
+
login: opts.login,
|
|
175
|
+
doNotCreate: opts.doNotCreate,
|
|
176
|
+
});
|
|
177
|
+
if (resp.userRoot === undefined) {
|
|
178
|
+
if (opts.doNotCreate) return undefined;
|
|
179
|
+
throw new Error("getUserRoot returned no userRoot entry");
|
|
180
|
+
}
|
|
181
|
+
return createSignedResourceId(
|
|
182
|
+
resp.userRoot.resourceId,
|
|
183
|
+
toResourceSignature(resp.userRoot.resourceSignature),
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private async getUserRootViaList(opts: { login?: string }): Promise<SignedResourceId>;
|
|
188
|
+
private async getUserRootViaList(opts: {
|
|
189
|
+
login?: string;
|
|
190
|
+
doNotCreate: false;
|
|
191
|
+
}): Promise<SignedResourceId>;
|
|
192
|
+
private async getUserRootViaList(opts: {
|
|
193
|
+
login?: string;
|
|
194
|
+
doNotCreate: true;
|
|
195
|
+
}): Promise<SignedResourceId | undefined>;
|
|
196
|
+
private async getUserRootViaList(
|
|
197
|
+
opts: { login?: string; doNotCreate?: boolean } = {},
|
|
198
|
+
): Promise<SignedResourceId | undefined> {
|
|
199
|
+
const responses = await this.ll.listUserResources({ login: opts.login, limit: 1 });
|
|
200
|
+
for (const msg of responses) {
|
|
201
|
+
if (msg.entry.oneofKind === "userRoot") {
|
|
202
|
+
return createSignedResourceId(
|
|
203
|
+
msg.entry.userRoot.resourceId,
|
|
204
|
+
toResourceSignature(msg.entry.userRoot.resourceSignature),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
throw new Error("listUserResources returned no userRoot entry");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
private async getDataLibrariesViaList(
|
|
212
|
+
opts: { login?: string } = {},
|
|
213
|
+
): Promise<ReadonlyMap<string, StorageInfo>> {
|
|
214
|
+
const responses = await this.ll.listUserResources({ login: opts.login });
|
|
215
|
+
|
|
216
|
+
// Collect all LS/* shared resources, separating V1 and V2
|
|
217
|
+
const v1Entries: StorageInfo[] = [];
|
|
218
|
+
const v2ResourceIds: { resourceId: SignedResourceId; resourceType: ResourceType }[] = [];
|
|
219
|
+
|
|
220
|
+
for (const msg of responses) {
|
|
221
|
+
if (msg.entry.oneofKind !== "sharedResource") continue;
|
|
222
|
+
const sr = msg.entry.sharedResource;
|
|
223
|
+
|
|
224
|
+
if (!sr.resourceType) continue;
|
|
225
|
+
const typeName = sr.resourceType.name;
|
|
226
|
+
const typeVersion = sr.resourceType.version;
|
|
227
|
+
if (!typeName.startsWith(LsStorageTypePrefix)) continue;
|
|
228
|
+
|
|
229
|
+
const rId = createSignedResourceId(sr.resourceId, toResourceSignature(sr.resourceSignature));
|
|
230
|
+
const rType: ResourceType = { name: typeName, version: typeVersion };
|
|
231
|
+
|
|
232
|
+
if (typeVersion === "2") {
|
|
233
|
+
v2ResourceIds.push({ resourceId: rId, resourceType: rType });
|
|
234
|
+
} else {
|
|
235
|
+
// V1 or unknown version: derive storageId from type name
|
|
236
|
+
const storageId = typeName.substring(LsStorageTypePrefix.length);
|
|
237
|
+
v1Entries.push({
|
|
238
|
+
storageId,
|
|
239
|
+
storageName: storageId,
|
|
240
|
+
resourceId: rId,
|
|
241
|
+
resourceType: rType,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Read V2 resource data in a single transaction
|
|
247
|
+
let v2Entries: StorageInfo[] = [];
|
|
248
|
+
if (v2ResourceIds.length > 0) {
|
|
249
|
+
v2Entries = await this.runTx(
|
|
250
|
+
"ReadLsStorageV2Data",
|
|
251
|
+
false,
|
|
252
|
+
NullSignedResourceId,
|
|
253
|
+
async (tx) => {
|
|
254
|
+
const entries: StorageInfo[] = [];
|
|
255
|
+
for (const { resourceId, resourceType } of v2ResourceIds) {
|
|
256
|
+
const rd = await tx.getResourceData(resourceId, false);
|
|
257
|
+
if (rd.data) {
|
|
258
|
+
const v2Data = JSON.parse(Buffer.from(rd.data).toString("utf-8")) as LsStorageV2Data;
|
|
259
|
+
entries.push({
|
|
260
|
+
storageId: v2Data.storageID,
|
|
261
|
+
storageName: v2Data.storageName,
|
|
262
|
+
resourceId,
|
|
263
|
+
resourceType,
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return entries;
|
|
268
|
+
},
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const result = new Map<string, StorageInfo>();
|
|
273
|
+
for (const entry of [...v1Entries, ...v2Entries]) {
|
|
274
|
+
result.set(entry.storageId, entry);
|
|
275
|
+
}
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private async getUserRootViaLegacy(opts: { login?: string }): Promise<SignedResourceId>;
|
|
280
|
+
private async getUserRootViaLegacy(opts: {
|
|
281
|
+
login?: string;
|
|
282
|
+
doNotCreateUserRoot: false;
|
|
283
|
+
}): Promise<SignedResourceId>;
|
|
284
|
+
private async getUserRootViaLegacy(opts: {
|
|
285
|
+
login?: string;
|
|
286
|
+
doNotCreateUserRoot: true;
|
|
287
|
+
}): Promise<SignedResourceId | undefined>;
|
|
288
|
+
private async getUserRootViaLegacy(
|
|
289
|
+
opts: { login?: string; doNotCreateUserRoot?: boolean } = {},
|
|
290
|
+
): Promise<SignedResourceId | undefined> {
|
|
291
|
+
const login = opts.login ?? this.authUser;
|
|
292
|
+
const mainRootName =
|
|
293
|
+
login === null ? AnonymousClientRoot : createHash("sha256").update(login).digest("hex");
|
|
294
|
+
|
|
295
|
+
return await this.runTx("initialization", true, NullSignedResourceId, async (tx) => {
|
|
296
|
+
if (await tx.checkResourceNameExists(mainRootName)) {
|
|
297
|
+
return await tx.getResourceByName(mainRootName);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (opts.doNotCreateUserRoot) {
|
|
301
|
+
return undefined;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const mainRoot = tx.createRoot(ClientRoot);
|
|
305
|
+
tx.setResourceName(mainRootName, mainRoot);
|
|
306
|
+
await tx.commit();
|
|
307
|
+
return await toGlobalResourceId(mainRoot);
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
private async getDataLibrariesViaLegacy(): Promise<ReadonlyMap<string, StorageInfo>> {
|
|
312
|
+
return await this.runTx("GetAvailableStorageIds", false, NullSignedResourceId, async (tx) => {
|
|
313
|
+
const lsProviderId = await tx.getResourceByName("LSProvider");
|
|
314
|
+
const provider = await tx.getResourceData(lsProviderId, true);
|
|
315
|
+
|
|
316
|
+
const result = new Map<string, StorageInfo>();
|
|
317
|
+
for (const field of provider.fields) {
|
|
318
|
+
if (field.type !== "Dynamic" || !isNotNullSignedResourceId(field.value)) continue;
|
|
319
|
+
if (!field.name.startsWith(LsProviderFieldPrefix)) continue;
|
|
320
|
+
|
|
321
|
+
const storageId = field.name.substring(LsProviderFieldPrefix.length);
|
|
322
|
+
result.set(storageId, {
|
|
323
|
+
storageId,
|
|
324
|
+
storageName: storageId,
|
|
325
|
+
resourceId: field.value,
|
|
326
|
+
resourceType: { name: `${LsStorageTypePrefix}${storageId}`, version: "1" },
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return result;
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
package/src/helpers/poll.ts
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import type { PlClient } from "../core/client";
|
|
2
2
|
import type { RetryOptions } from "@milaboratories/ts-helpers";
|
|
3
3
|
import { createRetryState, nextRetryStateOrError, notEmpty } from "@milaboratories/ts-helpers";
|
|
4
|
-
import type { FieldData, FieldType, ResourceData,
|
|
5
|
-
import {
|
|
4
|
+
import type { FieldData, FieldType, ResourceData, SignedResourceId } from "../core/types";
|
|
5
|
+
import {
|
|
6
|
+
isNotNullSignedResourceId,
|
|
7
|
+
isNullSignedResourceId,
|
|
8
|
+
resourceIdToString,
|
|
9
|
+
} from "../core/types";
|
|
6
10
|
import type { PlTransaction } from "../core/transaction";
|
|
7
11
|
import * as tp from "node:timers/promises";
|
|
8
12
|
|
|
@@ -38,7 +42,7 @@ export class PollResourceAccessor {
|
|
|
38
42
|
}
|
|
39
43
|
|
|
40
44
|
public async requireNoError(): Promise<PollResourceAccessor> {
|
|
41
|
-
if (
|
|
45
|
+
if (isNullSignedResourceId(this.data.error)) return this;
|
|
42
46
|
await this.tx.throwError(this.data.error, this.path);
|
|
43
47
|
// hmm... https://github.com/microsoft/TypeScript/issues/34955
|
|
44
48
|
return this;
|
|
@@ -73,10 +77,13 @@ export class PollResourceAccessor {
|
|
|
73
77
|
const path = [...this.path, name];
|
|
74
78
|
|
|
75
79
|
const fieldData = this.getFieldData(name, expectedType);
|
|
76
|
-
if (
|
|
80
|
+
if (
|
|
81
|
+
isNotNullSignedResourceId(fieldData.error) &&
|
|
82
|
+
(failOnError || isNullSignedResourceId(fieldData.value))
|
|
83
|
+
)
|
|
77
84
|
await this.tx.throwError(fieldData.error, path);
|
|
78
85
|
|
|
79
|
-
if (
|
|
86
|
+
if (isNullSignedResourceId(fieldData.value)) throw new ContinuePolling();
|
|
80
87
|
|
|
81
88
|
return await this.tx.get(fieldData.value, failOnError, path);
|
|
82
89
|
}
|
|
@@ -103,7 +110,7 @@ export class PollResourceAccessor {
|
|
|
103
110
|
return await this.getMultiObj(
|
|
104
111
|
ops,
|
|
105
112
|
...this.data.fields
|
|
106
|
-
.filter((f) => f.valueIsFinal ||
|
|
113
|
+
.filter((f) => f.valueIsFinal || isNotNullSignedResourceId(f.error))
|
|
107
114
|
.map((f) => f.name),
|
|
108
115
|
);
|
|
109
116
|
}
|
|
@@ -123,7 +130,7 @@ export class PollTxAccessor {
|
|
|
123
130
|
constructor(public readonly tx: PlTransaction) {}
|
|
124
131
|
|
|
125
132
|
public async get(
|
|
126
|
-
rid:
|
|
133
|
+
rid: SignedResourceId,
|
|
127
134
|
failOnError: boolean = true,
|
|
128
135
|
path: string[] = [],
|
|
129
136
|
): Promise<PollResourceAccessor> {
|
|
@@ -133,7 +140,7 @@ export class PollTxAccessor {
|
|
|
133
140
|
return accessor;
|
|
134
141
|
}
|
|
135
142
|
|
|
136
|
-
async throwError(error:
|
|
143
|
+
async throwError(error: SignedResourceId, path: string[] = []): Promise<never> {
|
|
137
144
|
const errorRes = await this.get(error);
|
|
138
145
|
const errorText = Buffer.from(notEmpty(errorRes.data.data)).toString();
|
|
139
146
|
throw new Error(`${path.join(" -> ")} = ${errorText}`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { FieldData } from "../core/types";
|
|
2
|
-
import {
|
|
2
|
+
import { isNotNullSignedResourceId } from "../core/types";
|
|
3
3
|
|
|
4
4
|
export function fieldResolved(data: Pick<FieldData, "value" | "error">) {
|
|
5
|
-
return
|
|
5
|
+
return isNotNullSignedResourceId(data.error) || isNotNullSignedResourceId(data.value);
|
|
6
6
|
}
|