@procedures/client 0.8.1 → 0.9.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 (56) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
  4. package/dist/adapters/websocket/client/transport.js +4 -12
  5. package/dist/adapters/websocket/client/transport.js.map +1 -1
  6. package/dist/browser.d.ts +2 -2
  7. package/dist/browser.d.ts.map +1 -1
  8. package/dist/browser.js +2 -0
  9. package/dist/browser.js.map +1 -1
  10. package/dist/events/publisher.d.ts +11 -1
  11. package/dist/events/publisher.d.ts.map +1 -1
  12. package/dist/events/publisher.js +27 -8
  13. package/dist/events/publisher.js.map +1 -1
  14. package/dist/procedures/collection/changes.d.ts +3 -3
  15. package/dist/procedures/collection/changes.d.ts.map +1 -1
  16. package/dist/procedures/collection/changes.js +4 -4
  17. package/dist/procedures/collection/changes.js.map +1 -1
  18. package/dist/procedures/collection/index.d.ts +2 -0
  19. package/dist/procedures/collection/index.d.ts.map +1 -1
  20. package/dist/procedures/collection/index.js +2 -0
  21. package/dist/procedures/collection/index.js.map +1 -1
  22. package/dist/procedures/collection/partition.d.ts +67 -0
  23. package/dist/procedures/collection/partition.d.ts.map +1 -0
  24. package/dist/procedures/collection/partition.js +82 -0
  25. package/dist/procedures/collection/partition.js.map +1 -0
  26. package/dist/procedures/collection/procedures.d.ts +4 -4
  27. package/dist/procedures/collection/procedures.d.ts.map +1 -1
  28. package/dist/procedures/collection/procedures.js +55 -55
  29. package/dist/procedures/collection/procedures.js.map +1 -1
  30. package/dist/procedures/index.d.ts +2 -2
  31. package/dist/procedures/index.d.ts.map +1 -1
  32. package/dist/procedures/index.js +1 -1
  33. package/dist/procedures/index.js.map +1 -1
  34. package/dist/procedures/invoke.d.ts +4 -2
  35. package/dist/procedures/invoke.d.ts.map +1 -1
  36. package/dist/procedures/invoke.js +1 -1
  37. package/dist/procedures/invoke.js.map +1 -1
  38. package/dist/procedures/types.d.ts +13 -0
  39. package/dist/procedures/types.d.ts.map +1 -1
  40. package/dist/procedures/types.js.map +1 -1
  41. package/dist/server/procedure-server.d.ts +30 -1
  42. package/dist/server/procedure-server.d.ts.map +1 -1
  43. package/dist/server/procedure-server.js +59 -11
  44. package/dist/server/procedure-server.js.map +1 -1
  45. package/package.json +3 -3
  46. package/src/adapters/websocket/client/transport.ts +4 -12
  47. package/src/browser.ts +6 -0
  48. package/src/events/publisher.ts +29 -9
  49. package/src/procedures/collection/changes.ts +4 -4
  50. package/src/procedures/collection/index.ts +4 -0
  51. package/src/procedures/collection/partition.ts +123 -0
  52. package/src/procedures/collection/procedures.ts +72 -73
  53. package/src/procedures/index.ts +5 -0
  54. package/src/procedures/invoke.ts +4 -2
  55. package/src/procedures/types.ts +16 -0
  56. package/src/server/procedure-server.ts +65 -12
@@ -73,8 +73,10 @@ export type InvocationErrorCode =
73
73
  * to such a host (`createHttpModule({ allow })` of `@procedures/http`).
74
74
  * - `topic-not-allowed`: the `allow` function of an `EventPublisher` refuses a topic of a
75
75
  * subscription (`documentation/EVENTS.md`).
76
+ * - `partition-not-allowed`: the partition resolver of the collection procedures refuses the
77
+ * partition of a call (roadmap CLI-17).
76
78
  */
77
- export type ForbiddenReason = CapabilityRefusalReason | "outside-root" | "host-not-allowed" | "topic-not-allowed";
79
+ export type ForbiddenReason = CapabilityRefusalReason | "outside-root" | "host-not-allowed" | "topic-not-allowed" | "partition-not-allowed";
78
80
 
79
81
  /** An error of the invocation itself. An error that the handler throws passes through unchanged. */
80
82
  export class InvocationError extends Error {
@@ -84,7 +86,7 @@ export class InvocationError extends Error {
84
86
  readonly retryable: boolean = false;
85
87
  /**
86
88
  * The reason of a `FORBIDDEN` error: `effects-undeclared`, `effects-exceeded`, `clearance`,
87
- * `deadline`, `outside-root`, `host-not-allowed` or `topic-not-allowed`.
89
+ * `deadline`, `outside-root`, `host-not-allowed`, `topic-not-allowed` or `partition-not-allowed`.
88
90
  */
89
91
  readonly reason: ForbiddenReason | undefined;
90
92
 
@@ -259,6 +259,22 @@ export interface RepositoryProvider {
259
259
  */
260
260
  getStorage<T>(collection: string): CollectionStorage<T>;
261
261
 
262
+ /**
263
+ * The storage of one partition of a collection (roadmap CLI-17), for example the items of one
264
+ * tenant. A repository without this method has no partitions: the collection procedures then
265
+ * refuse a call with a partition, and never give it the storage of all partitions.
266
+ */
267
+ getPartitionStorage?<T>(collection: string, partition: string): CollectionStorage<T>;
268
+
269
+ /** True when the collection has partitions (roadmap CLI-17). Then a call resolves its partition with `resolvePartition`. */
270
+ hasPartitions?(collection: string): boolean;
271
+
272
+ /**
273
+ * The partition of a call to a partitioned collection (roadmap CLI-17): from the context (the
274
+ * tenant of `ctx.principal`) and the partition that the call asks for. `null` refuses the call.
275
+ */
276
+ resolvePartition?(collection: string, ctx: ProcedureContext, requested: string | undefined): string | undefined | null | Promise<string | undefined | null>;
277
+
262
278
  /**
263
279
  * Check if a collection exists.
264
280
  *
@@ -18,7 +18,8 @@ import { pathToKey } from "../procedures/types.js";
18
18
  import { ProcedureRegistry, PROCEDURE_REGISTRY } from "../procedures/registry.js";
19
19
  import { createCollectionProcedures } from "../procedures/collection/procedures.js";
20
20
  import { InvocationError, invokeProcedure, type AdmitItem, type Interceptor, type Invocation } from "../procedures/invoke.js";
21
- import type { Capability, ClearanceOrder, ResourcesOf } from "../procedures/types.js";
21
+ import type { Capability, ClearanceOrder, ProcedureContext, ResourcesOf } from "../procedures/types.js";
22
+ import type { CollectionPartitions } from "../procedures/collection/partition.js";
22
23
  import { TIMEOUT_METADATA_KEY, effectsOf, joinNarrowing, narrowCapability } from "../procedures/capability.js";
23
24
  import { IdempotencyCache, idempotencyKeyOf, isRepeatable, type IdempotencyOptions } from "../procedures/idempotency.js";
24
25
  import { optionalMs, type Duration } from "../duration.js";
@@ -316,6 +317,8 @@ export function errorResponse(id: string, error: unknown): ServerResponse<unknow
316
317
  export class ProcedureServer extends Server implements RepositoryProvider {
317
318
  private readonly procedureRegistry: ProcedureRegistry;
318
319
  private readonly storages = new Map<string, CollectionStorage<unknown>>();
320
+ /** The partitions of each partitioned collection: their storages and their resolver (roadmap CLI-17) */
321
+ private readonly partitions = new Map<string, CollectionPartitions<unknown>>();
319
322
  /** For each storage: the calls in flight that took it, and the closes that wait for them */
320
323
  private readonly storageUses = new Map<CollectionStorage<unknown>, { calls: number; waiters: Array<() => void> }>();
321
324
  /** The clock of the wait of `useStorage` */
@@ -421,7 +424,54 @@ export class ProcedureServer extends Server implements RepositoryProvider {
421
424
  * @param collection - Collection name
422
425
  */
423
426
  hasCollection(collection: string): boolean {
424
- return this.storages.has(collection);
427
+ return this.storages.has(collection) || this.partitions.has(collection);
428
+ }
429
+
430
+ /**
431
+ * The storage of one partition of a collection (roadmap CLI-17).
432
+ *
433
+ * @throws Error if the collection has no partitions
434
+ */
435
+ getPartitionStorage<T>(collection: string, partition: string): CollectionStorage<T> {
436
+ const partitions = this.partitions.get(collection);
437
+ if (!partitions) {
438
+ throw new Error(`Collection has no partitions: ${collection}`);
439
+ }
440
+ return partitions.storageOf(partition) as CollectionStorage<T>;
441
+ }
442
+
443
+ /** True when the collection has partitions (roadmap CLI-17). */
444
+ hasPartitions(collection: string): boolean {
445
+ return this.partitions.has(collection);
446
+ }
447
+
448
+ /**
449
+ * The partition of a call to a partitioned collection (roadmap CLI-17), from the resolver of
450
+ * `usePartitions`. `null` refuses the call.
451
+ *
452
+ * @throws Error if the collection has no partitions
453
+ */
454
+ resolvePartition(collection: string, ctx: ProcedureContext, requested: string | undefined): string | undefined | null | Promise<string | undefined | null> {
455
+ const partitions = this.partitions.get(collection);
456
+ if (!partitions) {
457
+ throw new Error(`Collection has no partitions: ${collection}`);
458
+ }
459
+ return partitions.resolve(ctx, requested);
460
+ }
461
+
462
+ /**
463
+ * Configure the partitions of a collection (roadmap CLI-17). `resolve` gives the partition of
464
+ * each call from its context (the tenant of `ctx.principal`) and the partition that the call asks
465
+ * for: the collection procedures and the subscriptions of `EventPublisher` use it. `storageOf`
466
+ * gives the storage of a partition: give the same storage for the same partition each time.
467
+ *
468
+ * @param name - Collection name
469
+ * @param partitions - The storage of each partition, and the resolver
470
+ * @returns this (for chaining)
471
+ */
472
+ usePartitions<T>(name: string, partitions: CollectionPartitions<T>): this {
473
+ this.partitions.set(name, partitions as CollectionPartitions<unknown>);
474
+ return this;
425
475
  }
426
476
 
427
477
  // ===========================================================================
@@ -503,17 +553,20 @@ export class ProcedureServer extends Server implements RepositoryProvider {
503
553
  private callRepository(): { repository: RepositoryProvider; release: () => void } {
504
554
  const taken = new Set<CollectionStorage<unknown>>();
505
555
  let released = false;
556
+ const take = <T>(storage: CollectionStorage<unknown>): CollectionStorage<T> => {
557
+ if (!released && !taken.has(storage)) {
558
+ taken.add(storage);
559
+ const use = this.storageUses.get(storage) ?? { calls: 0, waiters: [] };
560
+ use.calls++;
561
+ this.storageUses.set(storage, use);
562
+ }
563
+ return storage as CollectionStorage<T>;
564
+ };
506
565
  const repository: RepositoryProvider = {
507
- getStorage: <T>(collection: string): CollectionStorage<T> => {
508
- const storage = this.getStorage<unknown>(collection);
509
- if (!released && !taken.has(storage)) {
510
- taken.add(storage);
511
- const use = this.storageUses.get(storage) ?? { calls: 0, waiters: [] };
512
- use.calls++;
513
- this.storageUses.set(storage, use);
514
- }
515
- return storage as CollectionStorage<T>;
516
- },
566
+ getStorage: <T>(collection: string): CollectionStorage<T> => take<T>(this.getStorage<unknown>(collection)),
567
+ getPartitionStorage: <T>(collection: string, partition: string): CollectionStorage<T> => take<T>(this.getPartitionStorage<unknown>(collection, partition)),
568
+ hasPartitions: (collection: string): boolean => this.hasPartitions(collection),
569
+ resolvePartition: (collection: string, ctx: ProcedureContext, requested: string | undefined) => this.resolvePartition(collection, ctx, requested),
517
570
  hasCollection: (collection: string): boolean => this.hasCollection(collection),
518
571
  };
519
572
  const release = (): void => {