@ferricstore/ferricstore 0.1.5 → 0.1.6

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 (62) hide show
  1. package/README.md +20 -1
  2. package/dist/index.cjs +187 -11
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +39 -7
  5. package/dist/index.d.ts +39 -7
  6. package/dist/index.js +187 -11
  7. package/dist/index.js.map +1 -1
  8. package/docs/api/assets/hierarchy.js +1 -1
  9. package/docs/api/assets/navigation.js +1 -1
  10. package/docs/api/assets/search.js +1 -1
  11. package/docs/api/classes/FerricStoreClient.html +8 -2
  12. package/docs/api/classes/FerricStoreError.html +1 -1
  13. package/docs/api/classes/FlowAlreadyExistsError.html +1 -1
  14. package/docs/api/classes/FlowNotFoundError.html +1 -1
  15. package/docs/api/classes/FlowWrongStateError.html +1 -1
  16. package/docs/api/classes/InvalidCommandError.html +1 -1
  17. package/docs/api/classes/LockHeldError.html +1 -1
  18. package/docs/api/classes/LockNotOwnedError.html +1 -1
  19. package/docs/api/classes/OverloadedError.html +1 -1
  20. package/docs/api/classes/StaleLeaseError.html +1 -1
  21. package/docs/api/classes/Workflow.html +3 -2
  22. package/docs/api/classes/WorkflowClient.html +2 -2
  23. package/docs/api/classes/WorkflowContext.html +2 -2
  24. package/docs/api/classes/WorkflowFlowCommands.html +2 -2
  25. package/docs/api/classes/WorkflowWorker.html +2 -2
  26. package/docs/api/hierarchy.html +1 -1
  27. package/docs/api/index.html +6 -1
  28. package/docs/api/interfaces/CancelOptions.html +2 -2
  29. package/docs/api/interfaces/CompleteOptions.html +2 -2
  30. package/docs/api/interfaces/CompleteOutcome.html +1 -1
  31. package/docs/api/interfaces/CreateManyOptions.html +1 -1
  32. package/docs/api/interfaces/FailOptions.html +2 -2
  33. package/docs/api/interfaces/FailOutcome.html +1 -1
  34. package/docs/api/interfaces/FerricStoreClientFromUrlOptions.html +1 -1
  35. package/docs/api/interfaces/FerricStoreClientOptions.html +1 -1
  36. package/docs/api/interfaces/FlowPolicyOptions.html +7 -0
  37. package/docs/api/interfaces/FlowStatePolicy.html +3 -0
  38. package/docs/api/interfaces/InvocationCreateOptions.html +4 -0
  39. package/docs/api/interfaces/MutateOptions.html +2 -2
  40. package/docs/api/interfaces/NamedValueMutation.html +1 -1
  41. package/docs/api/interfaces/ReadOptions.html +2 -2
  42. package/docs/api/interfaces/ReclaimOptions.html +1 -1
  43. package/docs/api/interfaces/RequestContext.html +4 -0
  44. package/docs/api/interfaces/RequestContextOptions.html +2 -0
  45. package/docs/api/interfaces/RetryOptions.html +2 -2
  46. package/docs/api/interfaces/RetryOutcome.html +1 -1
  47. package/docs/api/interfaces/SearchOptions.html +2 -2
  48. package/docs/api/interfaces/StateOptions.html +3 -2
  49. package/docs/api/interfaces/StateRegistration.html +3 -2
  50. package/docs/api/interfaces/TDigestCreateOptions.html +1 -1
  51. package/docs/api/interfaces/TDigestMergeOptions.html +1 -1
  52. package/docs/api/interfaces/TransitionOptions.html +2 -2
  53. package/docs/api/interfaces/TransitionOutcome.html +1 -1
  54. package/docs/api/interfaces/WorkflowOptions.html +2 -2
  55. package/docs/api/interfaces/WorkflowWorkerResult.html +2 -2
  56. package/docs/api/modules.html +1 -1
  57. package/docs/api/types/FlowStateMode.html +1 -0
  58. package/docs/api/types/FlowStatePolicyLike.html +1 -0
  59. package/docs/api/types/ManagementPairs.html +1 -1
  60. package/docs/api/types/SearchStateMeta.html +1 -1
  61. package/docs/api/types/WorkflowHandler.html +1 -1
  62. package/package.json +1 -1
package/README.md CHANGED
@@ -34,7 +34,7 @@ const { FerricStoreClient, JsonCodec } = require("@ferricstore/ferricstore");
34
34
  docker run -p 6388:6388 \
35
35
  -e FERRICSTORE_PROTECTED_MODE=false \
36
36
  -v ferricstore_data:/data \
37
- ghcr.io/ferricstore/ferricstore:0.7.2
37
+ ghcr.io/ferricstore/ferricstore:0.7.5
38
38
  ```
39
39
 
40
40
  ## Cluster-aware client
@@ -171,6 +171,25 @@ for (const job of jobs) {
171
171
  }
172
172
  ```
173
173
 
174
+ FIFO Flow state policy is opt-in per state:
175
+
176
+ ```ts
177
+ await flow.installPolicy("email", {
178
+ states: {
179
+ queued: { mode: "fifo" }
180
+ }
181
+ });
182
+
183
+ await flow.create("email-3", {
184
+ partitionKey: "tenant-a:email",
185
+ payload: Buffer.from("welcome"),
186
+ state: "queued",
187
+ type: "email"
188
+ });
189
+ ```
190
+
191
+ FIFO states require a `partitionKey`; priority is for parallel states.
192
+
174
193
  ## FerricStore KV And Data Structures
175
194
 
176
195
  The same client exposes typed helpers for FerricStore's Redis-compatible store commands:
package/dist/index.cjs CHANGED
@@ -574,14 +574,64 @@ function commandExec(args) {
574
574
  if (args.length === 0) {
575
575
  throw new FerricStoreError("command requires at least one argument");
576
576
  }
577
+ const commandArgs = args.slice(1);
578
+ const payload = {
579
+ args: commandArgs,
580
+ command: asText(args[0]).toUpperCase()
581
+ };
582
+ if (commandArgs.length >= 2 && commandTokenIs(commandArgs[commandArgs.length - 2], "REQUEST_CONTEXT")) {
583
+ const requestContext = normalizeRequestContext(commandArgs[commandArgs.length - 1]);
584
+ payload.args = commandArgs.slice(0, -2);
585
+ if (requestContext != null) {
586
+ payload.request_context = requestContext;
587
+ }
588
+ }
577
589
  return {
578
590
  opcode: OPCODES.commandExec,
579
- payload: {
580
- args: args.slice(1),
581
- command: asText(args[0]).toUpperCase()
582
- }
591
+ payload
583
592
  };
584
593
  }
594
+ function normalizeRequestContext(context) {
595
+ if (!isPlainObject(context)) {
596
+ return void 0;
597
+ }
598
+ const out = {};
599
+ const subject = optionalText(context.subject);
600
+ const tenant = optionalText(context.tenant);
601
+ const scopes = normalizeRequestContextScopes(context.scopes);
602
+ if (subject != null) {
603
+ out.subject = subject;
604
+ }
605
+ if (tenant != null) {
606
+ out.tenant = tenant;
607
+ }
608
+ if (scopes.length > 0) {
609
+ out.scopes = scopes;
610
+ }
611
+ return Object.keys(out).length > 0 ? out : void 0;
612
+ }
613
+ function normalizeRequestContextScopes(scopes) {
614
+ const values = typeof scopes === "string" || import_node_buffer.Buffer.isBuffer(scopes) || scopes instanceof Uint8Array ? asText(scopes).split(/\s+/u) : Array.isArray(scopes) ? scopes.map((scope) => optionalText(scope)).filter((scope) => scope != null) : [];
615
+ return [...new Set(values.filter((scope) => scope.length > 0))];
616
+ }
617
+ function optionalText(value) {
618
+ if (value == null || value === "") {
619
+ return void 0;
620
+ }
621
+ if (typeof value === "string" || typeof value === "number" || typeof value === "bigint" || typeof value === "boolean" || import_node_buffer.Buffer.isBuffer(value) || value instanceof Uint8Array) {
622
+ return asText(value);
623
+ }
624
+ return void 0;
625
+ }
626
+ function commandTokenIs(value, expected) {
627
+ if (typeof value === "string") {
628
+ return value.toUpperCase() === expected;
629
+ }
630
+ if (import_node_buffer.Buffer.isBuffer(value) || value instanceof Uint8Array) {
631
+ return asText(value).toUpperCase() === expected;
632
+ }
633
+ return false;
634
+ }
585
635
  function pipelineCommand(commands) {
586
636
  const compact = compactPipelinePayload(commands);
587
637
  if (compact != null) {
@@ -4442,6 +4492,59 @@ var FerricStoreClient = class _FerricStoreClient {
4442
4492
  );
4443
4493
  return Array.isArray(response) ? [...response] : [];
4444
4494
  }
4495
+ async invocationDefinitionPut(definition, options = {}) {
4496
+ return normalizeAdminResponse(
4497
+ await this.command(
4498
+ ...commandWithRequestContext(
4499
+ "INVOCATION.DEFINITION.PUT",
4500
+ [jsonArg(definition)],
4501
+ options.requestContext
4502
+ )
4503
+ )
4504
+ );
4505
+ }
4506
+ async invocationDefinitionGet(name, options = {}) {
4507
+ return normalizeAdminResponse(
4508
+ await this.command(
4509
+ ...commandWithRequestContext("INVOCATION.DEFINITION.GET", [name], options.requestContext)
4510
+ )
4511
+ );
4512
+ }
4513
+ async invocationDefinitionList(options = {}) {
4514
+ const response = normalizeAdminResponse(
4515
+ await this.command(
4516
+ ...commandWithRequestContext("INVOCATION.DEFINITION.LIST", [], options.requestContext)
4517
+ )
4518
+ );
4519
+ return Array.isArray(response) ? [...response] : [];
4520
+ }
4521
+ async invocationCreate(name, attrs, options = {}) {
4522
+ const envelope = { attrs };
4523
+ if (options.context != null) {
4524
+ envelope.context = options.context;
4525
+ }
4526
+ if (options.idempotencyKey != null) {
4527
+ envelope.idempotency_key = options.idempotencyKey;
4528
+ }
4529
+ return normalizeAdminResponse(
4530
+ await this.command(
4531
+ ...commandWithRequestContext("INVOCATION.CREATE", [name, jsonArg(envelope)], options.requestContext)
4532
+ )
4533
+ );
4534
+ }
4535
+ async invocationGet(id, options = {}) {
4536
+ return normalizeAdminResponse(
4537
+ await this.command(...commandWithRequestContext("INVOCATION.GET", [id], options.requestContext))
4538
+ );
4539
+ }
4540
+ async invocationPartitionList(name, options = {}) {
4541
+ const args = [name];
4542
+ append(args, "SCOPE", options.scope);
4543
+ const response = normalizeAdminResponse(
4544
+ await this.command(...commandWithRequestContext("INVOCATION.PARTITION.LIST", args, options.requestContext))
4545
+ );
4546
+ return Array.isArray(response) ? [...response] : [];
4547
+ }
4445
4548
  async create(id, options) {
4446
4549
  const currentNowMs = options.nowMs ?? nowMs();
4447
4550
  const args = [
@@ -4474,7 +4577,6 @@ var FerricStoreClient = class _FerricStoreClient {
4474
4577
  async enqueue(id, options) {
4475
4578
  return await this.create(id, {
4476
4579
  ...options,
4477
- priority: options.priority ?? 0,
4478
4580
  state: options.state ?? "queued"
4479
4581
  });
4480
4582
  }
@@ -4486,7 +4588,6 @@ var FerricStoreClient = class _FerricStoreClient {
4486
4588
  return await this.createMany(options.partitionKey, items, {
4487
4589
  ...options,
4488
4590
  independent: options.independent ?? true,
4489
- priority: options.priority ?? 0,
4490
4591
  state: options.state ?? "queued"
4491
4592
  });
4492
4593
  }
@@ -4501,7 +4602,6 @@ var FerricStoreClient = class _FerricStoreClient {
4501
4602
  const response = await this.createMany(bucket, groupItems, {
4502
4603
  ...options,
4503
4604
  independent: options.independent ?? true,
4504
- priority: options.priority ?? 0,
4505
4605
  state: options.state ?? "queued"
4506
4606
  });
4507
4607
  const expanded = expandManyResponse(response, indexedItems.length);
@@ -4654,8 +4754,7 @@ var FerricStoreClient = class _FerricStoreClient {
4654
4754
  ...options,
4655
4755
  includeState: options.includeState ?? false,
4656
4756
  jobOnly: true,
4657
- limit: options.limit ?? 100,
4658
- priority: options.priority ?? 0
4757
+ limit: options.limit ?? 100
4659
4758
  });
4660
4759
  }
4661
4760
  async reclaim(type, options) {
@@ -5006,9 +5105,14 @@ var FerricStoreClient = class _FerricStoreClient {
5006
5105
  const args = ["FLOW.POLICY.SET", type];
5007
5106
  append(args, "INDEXED_STATE_META", options.indexedStateMeta);
5008
5107
  append(args, "STATE", options.state);
5108
+ appendPolicyMode(args, options.state, options.mode);
5009
5109
  if (options.retry != null) {
5010
5110
  appendRetryPolicy(args, options.retry);
5011
5111
  }
5112
+ for (const [state, policy] of Object.entries(options.states ?? {})) {
5113
+ args.push("STATE", state);
5114
+ appendFlowStatePolicy(args, policy);
5115
+ }
5012
5116
  append(args, "RETENTION_TTL_MS", options.retentionTtlMs);
5013
5117
  return await this.command(...args);
5014
5118
  }
@@ -5388,6 +5492,16 @@ function appendSearchStateMeta(args, state, stateMeta) {
5388
5492
  function normalizeAdminResponse(value) {
5389
5493
  return normalizeRefMeta(value);
5390
5494
  }
5495
+ function jsonArg(value) {
5496
+ return typeof value === "string" ? value : JSON.stringify(value);
5497
+ }
5498
+ function commandWithRequestContext(command, args, requestContext) {
5499
+ const commandArgs = [command, ...args];
5500
+ if (requestContext != null) {
5501
+ commandArgs.push("REQUEST_CONTEXT", requestContext);
5502
+ }
5503
+ return commandArgs;
5504
+ }
5391
5505
  function infoText(value) {
5392
5506
  if (typeof value === "string" || Buffer.isBuffer(value)) {
5393
5507
  return text(value);
@@ -5419,6 +5533,19 @@ function managementPairArgs(pairs2) {
5419
5533
  }
5420
5534
  return args;
5421
5535
  }
5536
+ function appendFlowStatePolicy(args, policy) {
5537
+ if (isFlowStatePolicy(policy)) {
5538
+ appendPolicyMode(args, "state", policy.mode);
5539
+ if (policy.retry != null) {
5540
+ appendRetryPolicy(args, policy.retry);
5541
+ }
5542
+ return;
5543
+ }
5544
+ appendRetryPolicy(args, policy);
5545
+ }
5546
+ function isFlowStatePolicy(policy) {
5547
+ return typeof policy === "object" && policy != null && ("mode" in policy || "retry" in policy);
5548
+ }
5422
5549
  function ruleArgs(rules) {
5423
5550
  return typeof rules === "string" ? [rules] : [...rules];
5424
5551
  }
@@ -5511,6 +5638,24 @@ function appendRetryPolicy(args, policy) {
5511
5638
  append(args, "JITTER_PCT", policy.jitterPct);
5512
5639
  append(args, "EXHAUSTED_TO", policy.exhaustedTo);
5513
5640
  }
5641
+ function appendPolicyMode(args, state, mode) {
5642
+ if (mode == null) {
5643
+ return;
5644
+ }
5645
+ if (state == null) {
5646
+ throw new Error("policy mode requires state");
5647
+ }
5648
+ switch (mode) {
5649
+ case "fifo":
5650
+ args.push("MODE", "FIFO");
5651
+ return;
5652
+ case "parallel":
5653
+ args.push("MODE", "PARALLEL");
5654
+ return;
5655
+ default:
5656
+ throw new Error("policy mode must be 'fifo' or 'parallel'");
5657
+ }
5658
+ }
5514
5659
  function appendPayloadRead(args, payload, maxBytes) {
5515
5660
  if (payload === false) {
5516
5661
  args.push("NOPAYLOAD");
@@ -5975,6 +6120,7 @@ var Workflow = class {
5975
6120
  exceptionPolicy: normalizeExceptionPolicy(options.exceptionPolicy),
5976
6121
  handler,
5977
6122
  leaseMs: options.leaseMs ?? 3e4,
6123
+ ...options.mode == null ? {} : { mode: normalizeStateMode(options.mode) },
5978
6124
  name,
5979
6125
  returnRecord: options.returnRecord ?? false,
5980
6126
  valueMaxBytes: options.valueMaxBytes ?? this.valueConfig.valueMaxBytes,
@@ -6009,6 +6155,21 @@ var Workflow = class {
6009
6155
  worker(options = {}) {
6010
6156
  return new WorkflowWorker(this, options);
6011
6157
  }
6158
+ async installPolicy(options = {}) {
6159
+ const states = {};
6160
+ for (const registration of this.states.values()) {
6161
+ if (registration.mode != null || registration.retryPolicy != null) {
6162
+ states[registration.name] = {
6163
+ ...registration.mode == null ? {} : { mode: registration.mode },
6164
+ ...registration.retryPolicy == null ? {} : { retry: registration.retryPolicy }
6165
+ };
6166
+ }
6167
+ }
6168
+ return await this.client.installPolicy(this.type, {
6169
+ ...options,
6170
+ states
6171
+ });
6172
+ }
6012
6173
  stateNames() {
6013
6174
  return [...this.states.keys()];
6014
6175
  }
@@ -6269,13 +6430,15 @@ var WorkflowWorker = class {
6269
6430
  }
6270
6431
  async applyJob(job, registration) {
6271
6432
  const ctx = new WorkflowContext(this.workflow, job, registration.name);
6433
+ let outcome;
6272
6434
  try {
6273
6435
  const value = await registration.handler(ctx);
6274
- const outcome = isOutcome(value) ? value : complete({ result: value });
6275
- await applyOutcome(this.workflow.client, ctx, outcome, registration.returnRecord);
6436
+ outcome = isOutcome(value) ? value : complete({ result: value });
6276
6437
  } catch (error) {
6277
6438
  await this.applyHandlerError(ctx, registration, error);
6439
+ return;
6278
6440
  }
6441
+ await applyOutcome(this.workflow.client, ctx, outcome, registration.returnRecord);
6279
6442
  }
6280
6443
  async applyHandlerError(ctx, registration, error) {
6281
6444
  const policy = this.options.exceptionPolicy ?? registration.exceptionPolicy;
@@ -6306,6 +6469,7 @@ async function applyOutcome(client, ctx, outcome, returnRecord) {
6306
6469
  }
6307
6470
  }
6308
6471
  async function applyTransition(client, ctx, outcome, returnRecord) {
6472
+ validateTransitionPolicy(ctx, outcome);
6309
6473
  await client.transition(ctx.id, {
6310
6474
  dropValues: outcome.dropValues,
6311
6475
  fencingToken: ctx.fencingToken,
@@ -6323,6 +6487,12 @@ async function applyTransition(client, ctx, outcome, returnRecord) {
6323
6487
  values: outcome.values
6324
6488
  });
6325
6489
  }
6490
+ function validateTransitionPolicy(ctx, outcome) {
6491
+ const target = ctx.workflow.stateRegistration(outcome.toState);
6492
+ if (target?.mode === "fifo" && outcome.priority != null) {
6493
+ throw new Error("priority is not supported for fifo state");
6494
+ }
6495
+ }
6326
6496
  async function applyComplete(client, ctx, outcome, returnRecord) {
6327
6497
  await client.complete(ctx.id, {
6328
6498
  dropValues: outcome.dropValues,
@@ -6381,6 +6551,12 @@ function errorToPayload(error) {
6381
6551
  }
6382
6552
  return error;
6383
6553
  }
6554
+ function normalizeStateMode(mode) {
6555
+ if (mode === "fifo" || mode === "parallel") {
6556
+ return mode;
6557
+ }
6558
+ throw new Error("state mode must be 'fifo' or 'parallel'");
6559
+ }
6384
6560
  function valueRefToString(value) {
6385
6561
  if (typeof value === "string") {
6386
6562
  return value;