@xnetjs/core 0.11.1 → 0.12.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/index.d.ts CHANGED
@@ -864,6 +864,96 @@ declare function validateExternalUrl(rawUrl: string): {
864
864
  error?: string;
865
865
  };
866
866
 
867
+ /**
868
+ * Retry/backoff policies (exploration 0300).
869
+ *
870
+ * One dependency-free vocabulary for every reconnect/retry loop in the repo,
871
+ * replacing the hand-rolled backoff math that had drifted into three parallel
872
+ * implementations (sync connection-manager, WebSocketSyncProvider, webhook
873
+ * emitter). Deliberately shaped like Effect's `Schedule` combinators so a
874
+ * later migration would be mechanical — but scope-guarded: if this module
875
+ * needs union/intersect/cron/hedging, stop and re-read exploration 0300.
876
+ */
877
+ /**
878
+ * A retry policy maps a 1-based attempt number to the delay (in ms) to wait
879
+ * before that attempt, or `null` to give up retrying.
880
+ */
881
+ interface RetryPolicy {
882
+ /** Delay in ms before the given 1-based attempt, or `null` to give up. */
883
+ delayFor(attempt: number): number | null;
884
+ }
885
+ /** The same delay before every attempt. */
886
+ declare function fixed(delayMs: number): RetryPolicy;
887
+ /**
888
+ * Exponential backoff: `baseMs * factor^(attempt - 1)`.
889
+ * Attempt 1 waits `baseMs`, attempt 2 waits `baseMs * factor`, and so on.
890
+ */
891
+ declare function exponential(baseMs: number, factor?: number): RetryPolicy;
892
+ /** Cap another policy's delay at `maxDelayMs`. Passes `null` (give up) through. */
893
+ declare function capped(policy: RetryPolicy, maxDelayMs: number): RetryPolicy;
894
+ /**
895
+ * Add random jitter on top of another policy's delay: `delay + floor(random()
896
+ * * delay * ratio)`, i.e. up to `ratio` extra. Matches the hub rate-limit
897
+ * backoff behavior (exploration 0206) at the default `ratio` of 0.5. Passes
898
+ * `null` (give up) through. `random` is injectable for deterministic tests.
899
+ */
900
+ declare function jittered(policy: RetryPolicy, ratio?: number, random?: () => number): RetryPolicy;
901
+ /** Give up (return `null`) once `attempt` exceeds `maxAttempts`. */
902
+ declare function limitAttempts(policy: RetryPolicy, maxAttempts: number): RetryPolicy;
903
+
904
+ /**
905
+ * Tagged errors (exploration 0300).
906
+ *
907
+ * The repo-wide convention for structured errors, formalizing what
908
+ * `NodeRelayError.code` and friends already did by hand: every structured
909
+ * error carries a string-literal `_tag` discriminant so catch sites can
910
+ * narrow exhaustively instead of sniffing `instanceof` chains or message
911
+ * strings. Deliberately shaped like Effect's `Data.TaggedError` so a later
912
+ * migration would be mechanical.
913
+ *
914
+ * Convention (see CLAUDE.md): new structured errors extend `TaggedError`,
915
+ * set `_tag` to the class name, and put machine-readable context in readonly
916
+ * fields (a `code` union where one error class spans several failure kinds).
917
+ * Existing error classes migrate on touch, not as a campaign.
918
+ */
919
+ declare abstract class TaggedError<Tag extends string = string> extends Error {
920
+ /** String-literal discriminant — by convention the class name. */
921
+ abstract readonly _tag: Tag;
922
+ constructor(message: string, options?: ErrorOptions);
923
+ }
924
+ /** Narrow an unknown catch value to a specific tagged error. */
925
+ declare function isTagged<Tag extends string>(error: unknown, tag: Tag): error is TaggedError<Tag>;
926
+
927
+ /**
928
+ * Single-flight promise memoization (exploration 0300).
929
+ *
930
+ * The one implementation of the "share the in-flight promise so concurrent
931
+ * callers don't convoy the backend" pattern that had been independently
932
+ * hand-rolled at several call sites (schema registry lazy loads, sqlite
933
+ * adapter EXPLAIN diagnostics — explorations 0271/0276).
934
+ *
935
+ * The caller owns the map, so lifetime policy (instance field vs module
936
+ * scope), size caps, and explicit invalidation stay at the call site.
937
+ */
938
+ interface SingleFlightOptions {
939
+ /**
940
+ * What happens to the map entry after the promise settles:
941
+ *
942
+ * - `'settled'` (default): the entry is removed once the promise settles —
943
+ * the map only ever holds in-flight work (dedupe, not a cache).
944
+ * - `'keep'`: the entry stays after success, memoizing the result until the
945
+ * caller evicts it. Rejections are always removed, so a failure never
946
+ * poisons the key and the next caller retries.
947
+ */
948
+ retain?: 'settled' | 'keep';
949
+ }
950
+ /**
951
+ * Return the in-flight promise for `key` if one exists; otherwise start
952
+ * `fn()`, store its promise in `map`, and return it. Entry lifetime after
953
+ * settling is controlled by {@link SingleFlightOptions.retain}.
954
+ */
955
+ declare function singleFlight<K, V>(map: Map<K, Promise<V>>, key: K, fn: () => Promise<V>, options?: SingleFlightOptions): Promise<V>;
956
+
867
957
  /**
868
958
  * The ONE Last-Write-Wins ordering for xNet (docs/specs/protocol §L1.7,
869
959
  * exploration 0276).
@@ -926,4 +1016,4 @@ declare function lwwUpdateGuardSql(input: {
926
1016
  type DID = `did:key:${string}`;
927
1017
  type DocumentPath = `xnet://${DID}/workspace/${string}/doc/${string}`;
928
1018
 
929
- export { ALL_CAPABILITIES, AUTH_ACTIONS, type ActionKey, type AllowExpr, type AndExpr, type AuthAction, type AuthCheckInput, type AuthDecision, type AuthDenyReason, type AuthExpression, type AuthTrace, type AuthTraceStep, type AuthenticatedExpr, type AuthorizationDefinition, BOOTSTRAP_PEERS, type Capability, type ChainStatus, type Condition, type ContentChunk, type ContentId, type ContentResolver, type ContentTree, type CreatorRoleResolver, DEFAULT_SNAPSHOT_TRIGGERS, DEFAULT_STREAMING_OPTIONS, DHT_CONFIG, type DID, type DIDResolution, type DIDResolver, type DataSource, type DenyExpr, type DocumentLoad, type DocumentPath, type Fork, type Group, type IPCondition, type LwwStamp, type MembershipRoleResolver, type MerkleNode, type NotExpr, type OrExpr, type PeerLocation, type PermissionEvaluator, type PermissionGrant, type PolicyEvaluator, type PropertyRoleResolver, type PublicExpr, type Query, type QueryPlan, type QueryRequest, type QueryResponse, type QueryRouter, RESOLUTION_CACHE_CONFIG, type RelationRoleResolver, type ResolutionStrategy, type ResourceScope, type Role, type RoleKey, type RoleRefExpr, type RoleResolver, STANDARD_ROLES, type SchemaAction, type SerializedAuthExpression, type SerializedAuthorization, type SerializedRoleResolver, type SignedUpdate, type Snapshot, type SnapshotTriggers, SsrfError, type StreamingQueryOptions, type SubQuery, type TimeCondition, type UpdateVerifier, type VectorClock, assertPublicUrl, buildMerkleTree, clamp, clamp01, compareChangeApplicationOrder, compareLwwStamps, compareVectorClocks, createChunk, createContentId, deduplicatedUnion, detectFork, estimateQueryCost, evaluateCondition, formatBytes, getMostPermissiveCapability, hashContent, incrementVectorClock, isLocationFresh, isValidDID, isValidProgression, lwwUpdateGuardSql, lwwWins, mergeStateVectors, mergeVectorClocks, parseContentId, parseDID, roleHasCapability, shouldCreateSnapshot, unionAggregate, validateExternalUrl, verifyContent, verifyUpdateChain };
1019
+ export { ALL_CAPABILITIES, AUTH_ACTIONS, type ActionKey, type AllowExpr, type AndExpr, type AuthAction, type AuthCheckInput, type AuthDecision, type AuthDenyReason, type AuthExpression, type AuthTrace, type AuthTraceStep, type AuthenticatedExpr, type AuthorizationDefinition, BOOTSTRAP_PEERS, type Capability, type ChainStatus, type Condition, type ContentChunk, type ContentId, type ContentResolver, type ContentTree, type CreatorRoleResolver, DEFAULT_SNAPSHOT_TRIGGERS, DEFAULT_STREAMING_OPTIONS, DHT_CONFIG, type DID, type DIDResolution, type DIDResolver, type DataSource, type DenyExpr, type DocumentLoad, type DocumentPath, type Fork, type Group, type IPCondition, type LwwStamp, type MembershipRoleResolver, type MerkleNode, type NotExpr, type OrExpr, type PeerLocation, type PermissionEvaluator, type PermissionGrant, type PolicyEvaluator, type PropertyRoleResolver, type PublicExpr, type Query, type QueryPlan, type QueryRequest, type QueryResponse, type QueryRouter, RESOLUTION_CACHE_CONFIG, type RelationRoleResolver, type ResolutionStrategy, type ResourceScope, type RetryPolicy, type Role, type RoleKey, type RoleRefExpr, type RoleResolver, STANDARD_ROLES, type SchemaAction, type SerializedAuthExpression, type SerializedAuthorization, type SerializedRoleResolver, type SignedUpdate, type SingleFlightOptions, type Snapshot, type SnapshotTriggers, SsrfError, type StreamingQueryOptions, type SubQuery, TaggedError, type TimeCondition, type UpdateVerifier, type VectorClock, assertPublicUrl, buildMerkleTree, capped, clamp, clamp01, compareChangeApplicationOrder, compareLwwStamps, compareVectorClocks, createChunk, createContentId, deduplicatedUnion, detectFork, estimateQueryCost, evaluateCondition, exponential, fixed, formatBytes, getMostPermissiveCapability, hashContent, incrementVectorClock, isLocationFresh, isTagged, isValidDID, isValidProgression, jittered, limitAttempts, lwwUpdateGuardSql, lwwWins, mergeStateVectors, mergeVectorClocks, parseContentId, parseDID, roleHasCapability, shouldCreateSnapshot, singleFlight, unionAggregate, validateExternalUrl, verifyContent, verifyUpdateChain };
package/dist/index.js CHANGED
@@ -408,6 +408,67 @@ function validateExternalUrl(rawUrl) {
408
408
  }
409
409
  }
410
410
 
411
+ // src/retry/policy.ts
412
+ function fixed(delayMs) {
413
+ return {
414
+ delayFor: () => delayMs
415
+ };
416
+ }
417
+ function exponential(baseMs, factor = 2) {
418
+ return {
419
+ delayFor: (attempt) => baseMs * factor ** (attempt - 1)
420
+ };
421
+ }
422
+ function capped(policy, maxDelayMs) {
423
+ return {
424
+ delayFor: (attempt) => {
425
+ const delay = policy.delayFor(attempt);
426
+ return delay === null ? null : Math.min(delay, maxDelayMs);
427
+ }
428
+ };
429
+ }
430
+ function jittered(policy, ratio = 0.5, random = Math.random) {
431
+ return {
432
+ delayFor: (attempt) => {
433
+ const delay = policy.delayFor(attempt);
434
+ return delay === null ? null : delay + Math.floor(random() * delay * ratio);
435
+ }
436
+ };
437
+ }
438
+ function limitAttempts(policy, maxAttempts) {
439
+ return {
440
+ delayFor: (attempt) => attempt > maxAttempts ? null : policy.delayFor(attempt)
441
+ };
442
+ }
443
+
444
+ // src/errors/tagged.ts
445
+ var TaggedError = class extends Error {
446
+ constructor(message, options) {
447
+ super(message, options);
448
+ this.name = new.target.name;
449
+ }
450
+ };
451
+ function isTagged(error, tag) {
452
+ return error instanceof TaggedError && error._tag === tag;
453
+ }
454
+
455
+ // src/async/single-flight.ts
456
+ function singleFlight(map, key, fn, options = {}) {
457
+ const existing = map.get(key);
458
+ if (existing) return existing;
459
+ const promise = fn();
460
+ map.set(key, promise);
461
+ const evict = () => {
462
+ if (map.get(key) === promise) map.delete(key);
463
+ };
464
+ if ((options.retain ?? "settled") === "settled") {
465
+ promise.then(evict, evict);
466
+ } else {
467
+ promise.then(void 0, evict);
468
+ }
469
+ return promise;
470
+ }
471
+
411
472
  // src/lww.ts
412
473
  function compareLwwStamps(a, b) {
413
474
  if (a.lamport !== b.lamport) return a.lamport - b.lamport;
@@ -439,8 +500,10 @@ export {
439
500
  RESOLUTION_CACHE_CONFIG,
440
501
  STANDARD_ROLES,
441
502
  SsrfError,
503
+ TaggedError,
442
504
  assertPublicUrl,
443
505
  buildMerkleTree,
506
+ capped,
444
507
  clamp,
445
508
  clamp01,
446
509
  compareChangeApplicationOrder,
@@ -452,13 +515,18 @@ export {
452
515
  detectFork,
453
516
  estimateQueryCost,
454
517
  evaluateCondition,
518
+ exponential,
519
+ fixed,
455
520
  formatBytes,
456
521
  getMostPermissiveCapability,
457
522
  hashContent,
458
523
  incrementVectorClock,
459
524
  isLocationFresh,
525
+ isTagged,
460
526
  isValidDID,
461
527
  isValidProgression,
528
+ jittered,
529
+ limitAttempts,
462
530
  lwwUpdateGuardSql,
463
531
  lwwWins,
464
532
  mergeStateVectors,
@@ -467,6 +535,7 @@ export {
467
535
  parseDID,
468
536
  roleHasCapability,
469
537
  shouldCreateSnapshot,
538
+ singleFlight,
470
539
  unionAggregate,
471
540
  validateExternalUrl,
472
541
  verifyContent,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xnetjs/core",
3
- "version": "0.11.1",
3
+ "version": "0.12.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -25,6 +25,7 @@
25
25
  "provenance": true
26
26
  },
27
27
  "devDependencies": {
28
+ "fast-check": "^4.8.0",
28
29
  "tsup": "^8.0.0",
29
30
  "typescript": "^5.4.0"
30
31
  },