@milaboratories/pl-client 3.2.4 → 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.
Files changed (112) hide show
  1. package/dist/core/client.cjs +24 -56
  2. package/dist/core/client.cjs.map +1 -1
  3. package/dist/core/client.d.ts +12 -8
  4. package/dist/core/client.d.ts.map +1 -1
  5. package/dist/core/client.js +26 -58
  6. package/dist/core/client.js.map +1 -1
  7. package/dist/core/errors.cjs +20 -0
  8. package/dist/core/errors.cjs.map +1 -1
  9. package/dist/core/errors.d.ts +6 -1
  10. package/dist/core/errors.d.ts.map +1 -1
  11. package/dist/core/errors.js +19 -1
  12. package/dist/core/errors.js.map +1 -1
  13. package/dist/core/final.cjs +6 -5
  14. package/dist/core/final.cjs.map +1 -1
  15. package/dist/core/final.d.ts.map +1 -1
  16. package/dist/core/final.js +7 -6
  17. package/dist/core/final.js.map +1 -1
  18. package/dist/core/ll_client.cjs +18 -1
  19. package/dist/core/ll_client.cjs.map +1 -1
  20. package/dist/core/ll_client.d.ts +6 -2
  21. package/dist/core/ll_client.d.ts.map +1 -1
  22. package/dist/core/ll_client.js +19 -2
  23. package/dist/core/ll_client.js.map +1 -1
  24. package/dist/core/transaction.cjs +109 -75
  25. package/dist/core/transaction.cjs.map +1 -1
  26. package/dist/core/transaction.d.ts +30 -22
  27. package/dist/core/transaction.d.ts.map +1 -1
  28. package/dist/core/transaction.js +111 -76
  29. package/dist/core/transaction.js.map +1 -1
  30. package/dist/core/type_conversion.cjs +14 -6
  31. package/dist/core/type_conversion.cjs.map +1 -1
  32. package/dist/core/type_conversion.js +14 -6
  33. package/dist/core/type_conversion.js.map +1 -1
  34. package/dist/core/types.cjs +77 -17
  35. package/dist/core/types.cjs.map +1 -1
  36. package/dist/core/types.d.ts +49 -26
  37. package/dist/core/types.d.ts.map +1 -1
  38. package/dist/core/types.js +66 -14
  39. package/dist/core/types.js.map +1 -1
  40. package/dist/core/user_resources.cjs +181 -0
  41. package/dist/core/user_resources.cjs.map +1 -0
  42. package/dist/core/user_resources.d.ts +75 -0
  43. package/dist/core/user_resources.d.ts.map +1 -0
  44. package/dist/core/user_resources.js +180 -0
  45. package/dist/core/user_resources.js.map +1 -0
  46. package/dist/helpers/poll.cjs +4 -4
  47. package/dist/helpers/poll.cjs.map +1 -1
  48. package/dist/helpers/poll.d.ts +3 -3
  49. package/dist/helpers/poll.d.ts.map +1 -1
  50. package/dist/helpers/poll.js +5 -5
  51. package/dist/helpers/poll.js.map +1 -1
  52. package/dist/helpers/tx_helpers.cjs +1 -1
  53. package/dist/helpers/tx_helpers.cjs.map +1 -1
  54. package/dist/helpers/tx_helpers.d.ts +3 -3
  55. package/dist/helpers/tx_helpers.d.ts.map +1 -1
  56. package/dist/helpers/tx_helpers.js +2 -2
  57. package/dist/helpers/tx_helpers.js.map +1 -1
  58. package/dist/index.cjs +16 -5
  59. package/dist/index.d.ts +5 -4
  60. package/dist/index.js +5 -4
  61. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.cjs +724 -188
  62. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.cjs.map +1 -1
  63. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.cjs +34 -9
  64. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.cjs.map +1 -1
  65. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.d.ts +37 -5
  66. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.d.ts.map +1 -1
  67. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.js +34 -9
  68. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.js.map +1 -1
  69. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.d.ts +326 -136
  70. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.d.ts.map +1 -1
  71. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.js +724 -188
  72. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.js.map +1 -1
  73. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.cjs +18 -7
  74. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.cjs.map +1 -1
  75. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.d.ts +11 -7
  76. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.d.ts.map +1 -1
  77. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.js +18 -7
  78. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.js.map +1 -1
  79. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.cjs +57 -2
  80. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.cjs.map +1 -1
  81. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.d.ts +26 -3
  82. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.d.ts.map +1 -1
  83. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.js +57 -3
  84. package/dist/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.js.map +1 -1
  85. package/dist/proto-rest/plapi.d.ts +421 -207
  86. package/dist/proto-rest/plapi.d.ts.map +1 -1
  87. package/dist/test/test_config.cjs +6 -3
  88. package/dist/test/test_config.cjs.map +1 -1
  89. package/dist/test/test_config.d.ts.map +1 -1
  90. package/dist/test/test_config.js +7 -4
  91. package/dist/test/test_config.js.map +1 -1
  92. package/package.json +5 -5
  93. package/src/core/client.ts +58 -103
  94. package/src/core/errors.ts +23 -0
  95. package/src/core/final.ts +16 -6
  96. package/src/core/ll_client.ts +39 -3
  97. package/src/core/ll_transaction.test.ts +41 -6
  98. package/src/core/transaction.ts +176 -86
  99. package/src/core/type_conversion.ts +24 -9
  100. package/src/core/types.ts +147 -41
  101. package/src/core/user_resources.ts +332 -0
  102. package/src/helpers/poll.ts +15 -8
  103. package/src/helpers/state_helpers.ts +2 -2
  104. package/src/helpers/tx_helpers.ts +5 -5
  105. package/src/index.ts +1 -0
  106. package/src/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.client.ts +61 -14
  107. package/src/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api.ts +1045 -379
  108. package/src/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/api_types.ts +33 -18
  109. package/src/proto-grpc/github.com/milaboratory/pl/plapi/plapiproto/base_types.ts +75 -6
  110. package/src/proto-grpc/google/protobuf/descriptor.ts +5 -2
  111. package/src/proto-rest/plapi.ts +447 -225
  112. 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
- // more details here: https://egghead.io/blog/using-branded-types-in-typescript
4
- declare const __resource_id_type__: unique symbol;
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 ResourceId = BrandResourceId<"global">;
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 = BrandResourceId<"local">;
11
+ export type LocalResourceId = Branded<bigint, "local", "__resource_id__">;
15
12
 
16
13
  /** Any non-null resource id */
17
- export type AnyResourceId = ResourceId | LocalResourceId;
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 | ResourceId | LocalResourceId;
17
+ export type OptionalAnyResourceId = NullResourceId | GlobalResourceId | LocalResourceId;
24
18
 
25
19
  export const NullResourceId = 0n as NullResourceId;
26
20
 
27
- export function isNullResourceId(resourceId: bigint): resourceId is NullResourceId {
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: ResourceId;
74
- readonly originalResourceId: OptionalResourceId;
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: OptionalResourceId;
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: OptionalResourceId;
136
- readonly error: OptionalResourceId;
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): ResourceId {
190
- return ((isRoot ? ResourceIdRootMask : 0n) | unmaskedId) as ResourceId;
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(resourceId: OptionalAnyResourceId): string {
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
- /** Converts bigint to global resource id */
238
- export function bigintToResourceId(resourceId: bigint): ResourceId {
239
- if (isLocalResourceId(resourceId))
240
- throw new Error(`Local resource id: ${resourceIdToString(resourceId)}`);
241
- if (isNullResourceId(resourceId)) throw new Error(`Null resource id.`);
242
- return resourceId as ResourceId;
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" ? resourceIdToString(value as OptionalAnyResourceId) : value,
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
+ }
@@ -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, ResourceId } from "../core/types";
5
- import { isNotNullResourceId, isNullResourceId, resourceIdToString } from "../core/types";
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 (isNullResourceId(this.data.error)) return this;
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 (isNotNullResourceId(fieldData.error) && (failOnError || isNullResourceId(fieldData.value)))
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 (isNullResourceId(fieldData.value)) throw new ContinuePolling();
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 || isNotNullResourceId(f.error))
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: ResourceId,
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: ResourceId, path: string[] = []): Promise<never> {
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 { isNotNullResourceId } from "../core/types";
2
+ import { isNotNullSignedResourceId } from "../core/types";
3
3
 
4
4
  export function fieldResolved(data: Pick<FieldData, "value" | "error">) {
5
- return isNotNullResourceId(data.error) || isNotNullResourceId(data.value);
5
+ return isNotNullSignedResourceId(data.error) || isNotNullSignedResourceId(data.value);
6
6
  }