@xnetjs/core 0.11.1 → 1.0.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 +162 -19
- package/dist/index.js +141 -7
- package/package.json +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -385,9 +385,13 @@ interface Role {
|
|
|
385
385
|
capabilities: Capability[];
|
|
386
386
|
}
|
|
387
387
|
/**
|
|
388
|
-
* Available capabilities
|
|
388
|
+
* Available capabilities.
|
|
389
|
+
*
|
|
390
|
+
* `create`/`update` are refinements of `write` (exploration 0304): a role
|
|
391
|
+
* holding `write` implicitly covers both; the granular capabilities cover
|
|
392
|
+
* only themselves.
|
|
389
393
|
*/
|
|
390
|
-
type Capability = 'read' | 'write' | 'delete' | 'share' | 'admin';
|
|
394
|
+
type Capability = 'read' | 'create' | 'update' | 'write' | 'delete' | 'share' | 'admin';
|
|
391
395
|
/**
|
|
392
396
|
* All capabilities in order of privilege
|
|
393
397
|
*/
|
|
@@ -451,7 +455,8 @@ interface PermissionEvaluator {
|
|
|
451
455
|
*/
|
|
452
456
|
declare const STANDARD_ROLES: Record<string, Role>;
|
|
453
457
|
/**
|
|
454
|
-
* Check if a capability is included in a role
|
|
458
|
+
* Check if a capability is included in a role.
|
|
459
|
+
* A role holding `write` covers the `create`/`update` refinements.
|
|
455
460
|
*/
|
|
456
461
|
declare function roleHasCapability(role: Role, capability: Capability): boolean;
|
|
457
462
|
/**
|
|
@@ -483,12 +488,46 @@ type SchemaIRI = `xnet://${string}/${string}`;
|
|
|
483
488
|
/**
|
|
484
489
|
* Canonical authorization actions.
|
|
485
490
|
* All action checks map to one of these values.
|
|
491
|
+
*
|
|
492
|
+
* `create` and `update` are refinements of the coarse `write` action
|
|
493
|
+
* (exploration 0304): `create` governs bringing a node into existence,
|
|
494
|
+
* `update` governs mutating a node that already exists. A schema MAY declare
|
|
495
|
+
* either refinement; when one is absent its expression falls back to the
|
|
496
|
+
* schema's `write` expression (see {@link actionExpressionOrder}), so schemas
|
|
497
|
+
* that only declare `write` behave exactly as before. Checking `write` on an
|
|
498
|
+
* existing node is equivalent to checking `update`.
|
|
499
|
+
*
|
|
500
|
+
* `create` checks evaluate roles against the *draft* node built from the
|
|
501
|
+
* create payload (`createdBy` = the caller). Because `role.creator()` then
|
|
502
|
+
* always matches, a schema that wants real admission control must exclude the
|
|
503
|
+
* creator role from `create` and require a container role instead — the
|
|
504
|
+
* draft's relation properties (e.g. `space`, `channel`) resolve normally.
|
|
486
505
|
*/
|
|
487
|
-
declare const AUTH_ACTIONS: readonly ["read", "write", "delete", "share", "admin"];
|
|
506
|
+
declare const AUTH_ACTIONS: readonly ["read", "create", "update", "write", "delete", "share", "admin"];
|
|
488
507
|
/**
|
|
489
508
|
* An authorization action that can be checked or granted.
|
|
490
509
|
*/
|
|
491
510
|
type AuthAction = (typeof AUTH_ACTIONS)[number];
|
|
511
|
+
/**
|
|
512
|
+
* Expression lookup order for a checked action — the first action name whose
|
|
513
|
+
* expression a schema defines wins (exploration 0304 fallback table):
|
|
514
|
+
*
|
|
515
|
+
* | checked | expression used |
|
|
516
|
+
* |--------------------|-------------------------------------|
|
|
517
|
+
* | `create` | `actions.create` ?? `actions.write` |
|
|
518
|
+
* | `update` / `write` | `actions.update` ?? `actions.write` |
|
|
519
|
+
* | anything else | `actions[action]` |
|
|
520
|
+
*/
|
|
521
|
+
declare function actionExpressionOrder(action: AuthAction): readonly AuthAction[];
|
|
522
|
+
/**
|
|
523
|
+
* Whether a granted action satisfies a checked action.
|
|
524
|
+
*
|
|
525
|
+
* A `write` grant covers its refinements (`create`, `update`); a granular
|
|
526
|
+
* grant covers only itself. A granular grant does NOT satisfy a legacy
|
|
527
|
+
* `write` check (fail closed — old callers checking `write` on an existing
|
|
528
|
+
* node mean update, which `update` covers).
|
|
529
|
+
*/
|
|
530
|
+
declare function grantActionSatisfies(granted: AuthAction, checked: AuthAction): boolean;
|
|
492
531
|
/**
|
|
493
532
|
* The result of an authorization check.
|
|
494
533
|
* Contains whether access is allowed plus diagnostic info for debugging.
|
|
@@ -554,7 +593,11 @@ interface AuthTraceStep {
|
|
|
554
593
|
* },
|
|
555
594
|
* actions: {
|
|
556
595
|
* read: allow('editor', 'admin', 'owner'),
|
|
596
|
+
* // coarse mutation policy; also the fallback for create/update
|
|
557
597
|
* write: allow('editor', 'admin', 'owner'),
|
|
598
|
+
* // optional refinements (0304): who may add vs. who may modify
|
|
599
|
+
* create: allow('editor', 'admin'),
|
|
600
|
+
* update: allow('owner', 'admin'),
|
|
558
601
|
* delete: allow('admin', 'owner'),
|
|
559
602
|
* share: allow('admin', 'owner')
|
|
560
603
|
* },
|
|
@@ -865,26 +908,117 @@ declare function validateExternalUrl(rawUrl: string): {
|
|
|
865
908
|
};
|
|
866
909
|
|
|
867
910
|
/**
|
|
868
|
-
*
|
|
869
|
-
*
|
|
911
|
+
* Retry/backoff policies (exploration 0303).
|
|
912
|
+
*
|
|
913
|
+
* One dependency-free vocabulary for every reconnect/retry loop in the repo,
|
|
914
|
+
* replacing the hand-rolled backoff math that had drifted into three parallel
|
|
915
|
+
* implementations (sync connection-manager, WebSocketSyncProvider, webhook
|
|
916
|
+
* emitter). Deliberately shaped like Effect's `Schedule` combinators so a
|
|
917
|
+
* later migration would be mechanical — but scope-guarded: if this module
|
|
918
|
+
* needs union/intersect/cron/hedging, stop and re-read exploration 0303.
|
|
919
|
+
*/
|
|
920
|
+
/**
|
|
921
|
+
* A retry policy maps a 1-based attempt number to the delay (in ms) to wait
|
|
922
|
+
* before that attempt, or `null` to give up retrying.
|
|
923
|
+
*/
|
|
924
|
+
interface RetryPolicy {
|
|
925
|
+
/** Delay in ms before the given 1-based attempt, or `null` to give up. */
|
|
926
|
+
delayFor(attempt: number): number | null;
|
|
927
|
+
}
|
|
928
|
+
/** The same delay before every attempt. */
|
|
929
|
+
declare function fixed(delayMs: number): RetryPolicy;
|
|
930
|
+
/**
|
|
931
|
+
* Exponential backoff: `baseMs * factor^(attempt - 1)`.
|
|
932
|
+
* Attempt 1 waits `baseMs`, attempt 2 waits `baseMs * factor`, and so on.
|
|
933
|
+
*/
|
|
934
|
+
declare function exponential(baseMs: number, factor?: number): RetryPolicy;
|
|
935
|
+
/** Cap another policy's delay at `maxDelayMs`. Passes `null` (give up) through. */
|
|
936
|
+
declare function capped(policy: RetryPolicy, maxDelayMs: number): RetryPolicy;
|
|
937
|
+
/**
|
|
938
|
+
* Add random jitter on top of another policy's delay: `delay + floor(random()
|
|
939
|
+
* * delay * ratio)`, i.e. up to `ratio` extra. Matches the hub rate-limit
|
|
940
|
+
* backoff behavior (exploration 0206) at the default `ratio` of 0.5. Passes
|
|
941
|
+
* `null` (give up) through. `random` is injectable for deterministic tests.
|
|
942
|
+
*/
|
|
943
|
+
declare function jittered(policy: RetryPolicy, ratio?: number, random?: () => number): RetryPolicy;
|
|
944
|
+
/** Give up (return `null`) once `attempt` exceeds `maxAttempts`. */
|
|
945
|
+
declare function limitAttempts(policy: RetryPolicy, maxAttempts: number): RetryPolicy;
|
|
946
|
+
|
|
947
|
+
/**
|
|
948
|
+
* Tagged errors (exploration 0303).
|
|
949
|
+
*
|
|
950
|
+
* The repo-wide convention for structured errors, formalizing what
|
|
951
|
+
* `NodeRelayError.code` and friends already did by hand: every structured
|
|
952
|
+
* error carries a string-literal `_tag` discriminant so catch sites can
|
|
953
|
+
* narrow exhaustively instead of sniffing `instanceof` chains or message
|
|
954
|
+
* strings. Deliberately shaped like Effect's `Data.TaggedError` so a later
|
|
955
|
+
* migration would be mechanical.
|
|
956
|
+
*
|
|
957
|
+
* Convention (see CLAUDE.md): new structured errors extend `TaggedError`,
|
|
958
|
+
* set `_tag` to the class name, and put machine-readable context in readonly
|
|
959
|
+
* fields (a `code` union where one error class spans several failure kinds).
|
|
960
|
+
* Existing error classes migrate on touch, not as a campaign.
|
|
961
|
+
*/
|
|
962
|
+
declare abstract class TaggedError<Tag extends string = string> extends Error {
|
|
963
|
+
/** String-literal discriminant — by convention the class name. */
|
|
964
|
+
abstract readonly _tag: Tag;
|
|
965
|
+
constructor(message: string, options?: ErrorOptions);
|
|
966
|
+
}
|
|
967
|
+
/** Narrow an unknown catch value to a specific tagged error. */
|
|
968
|
+
declare function isTagged<Tag extends string>(error: unknown, tag: Tag): error is TaggedError<Tag>;
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* Single-flight promise memoization (exploration 0303).
|
|
972
|
+
*
|
|
973
|
+
* The one implementation of the "share the in-flight promise so concurrent
|
|
974
|
+
* callers don't convoy the backend" pattern that had been independently
|
|
975
|
+
* hand-rolled at several call sites (schema registry lazy loads, sqlite
|
|
976
|
+
* adapter EXPLAIN diagnostics — explorations 0271/0276).
|
|
870
977
|
*
|
|
871
|
-
*
|
|
872
|
-
*
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
978
|
+
* The caller owns the map, so lifetime policy (instance field vs module
|
|
979
|
+
* scope), size caps, and explicit invalidation stay at the call site.
|
|
980
|
+
*/
|
|
981
|
+
interface SingleFlightOptions {
|
|
982
|
+
/**
|
|
983
|
+
* What happens to the map entry after the promise settles:
|
|
984
|
+
*
|
|
985
|
+
* - `'settled'` (default): the entry is removed once the promise settles —
|
|
986
|
+
* the map only ever holds in-flight work (dedupe, not a cache).
|
|
987
|
+
* - `'keep'`: the entry stays after success, memoizing the result until the
|
|
988
|
+
* caller evicts it. Rejections are always removed, so a failure never
|
|
989
|
+
* poisons the key and the next caller retries.
|
|
990
|
+
*/
|
|
991
|
+
retain?: 'settled' | 'keep';
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Return the in-flight promise for `key` if one exists; otherwise start
|
|
995
|
+
* `fn()`, store its promise in `map`, and return it. Entry lifetime after
|
|
996
|
+
* settling is controlled by {@link SingleFlightOptions.retain}.
|
|
997
|
+
*/
|
|
998
|
+
declare function singleFlight<K, V>(map: Map<K, Promise<V>>, key: K, fn: () => Promise<V>, options?: SingleFlightOptions): Promise<V>;
|
|
999
|
+
|
|
1000
|
+
/** Protocol version at which the grinding-resistant tiebreak key activates. */
|
|
1001
|
+
declare const LWW_TIEBREAK_KEY_VERSION = 4;
|
|
1002
|
+
/**
|
|
1003
|
+
* The grinding-resistant LWW final tiebreak key (exploration 0305):
|
|
1004
|
+
* `blake3_hex( author ‖ US ‖ propertyKey ‖ US ‖ canonicalJSON(value) )`.
|
|
877
1005
|
*
|
|
878
|
-
*
|
|
879
|
-
*
|
|
880
|
-
*
|
|
881
|
-
* `0004-tie-author-case-codeunit` golden vector).
|
|
1006
|
+
* Portable by construction — a kernel in any language reproduces it from
|
|
1007
|
+
* canonical JSON of the value (which every kernel already computes for the
|
|
1008
|
+
* change hash). A deletion (`value === undefined`) canonicalises as `null`.
|
|
882
1009
|
*/
|
|
883
|
-
|
|
1010
|
+
declare function computeLwwTiebreakKey(author: string, propertyKey: string, value: unknown): string;
|
|
1011
|
+
/** The timestamp every LWW comparison runs on. */
|
|
884
1012
|
interface LwwStamp {
|
|
885
1013
|
lamport: number;
|
|
886
1014
|
wallTime: number;
|
|
887
1015
|
author: string;
|
|
1016
|
+
/**
|
|
1017
|
+
* Grinding-resistant tiebreak key ({@link computeLwwTiebreakKey}), present
|
|
1018
|
+
* only for changes at protocol version ≥ {@link LWW_TIEBREAK_KEY_VERSION}.
|
|
1019
|
+
* Absent (legacy) stamps fall back to the author-DID tiebreak.
|
|
1020
|
+
*/
|
|
1021
|
+
tiebreakKey?: string;
|
|
888
1022
|
}
|
|
889
1023
|
/**
|
|
890
1024
|
* Spec comparator (§L1.7): negative when `a` loses to `b`, positive when `a`
|
|
@@ -910,13 +1044,22 @@ declare function compareChangeApplicationOrder(a: {
|
|
|
910
1044
|
* SQL `ON CONFLICT … DO UPDATE … WHERE` guard implementing {@link lwwWins}
|
|
911
1045
|
* inside SQLite (the `excluded.` pseudo-table is the incoming row). Column
|
|
912
1046
|
* text comparison (`>`) is byte order under SQLite's default BINARY
|
|
913
|
-
* collation, which matches the code-unit rule for our ASCII DID strings.
|
|
1047
|
+
* collation, which matches the code-unit rule for our ASCII DID/hex strings.
|
|
1048
|
+
*
|
|
1049
|
+
* When `tiebreakKeyColumn` is supplied, the final rung mirrors
|
|
1050
|
+
* {@link compareLwwStamps}: on a `lamport`+`wallTime` tie, if BOTH rows carry a
|
|
1051
|
+
* non-null tiebreak key the larger key wins outright (author irrelevant);
|
|
1052
|
+
* otherwise (either key null, or keys equal) the author DID decides. The stored
|
|
1053
|
+
* key is precomputed in application code ({@link computeLwwTiebreakKey}), so SQL
|
|
1054
|
+
* only ever compares the opaque hex — never recomputes it — keeping the JS and
|
|
1055
|
+
* SQL paths byte-identical without a user-defined function.
|
|
914
1056
|
*/
|
|
915
1057
|
declare function lwwUpdateGuardSql(input: {
|
|
916
1058
|
table: string;
|
|
917
1059
|
lamportColumn: string;
|
|
918
1060
|
wallTimeColumn: string;
|
|
919
1061
|
authorColumn: string;
|
|
1062
|
+
tiebreakKeyColumn?: string;
|
|
920
1063
|
}): string;
|
|
921
1064
|
|
|
922
1065
|
/**
|
|
@@ -926,4 +1069,4 @@ declare function lwwUpdateGuardSql(input: {
|
|
|
926
1069
|
type DID = `did:key:${string}`;
|
|
927
1070
|
type DocumentPath = `xnet://${DID}/workspace/${string}/doc/${string}`;
|
|
928
1071
|
|
|
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 };
|
|
1072
|
+
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, LWW_TIEBREAK_KEY_VERSION, 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, actionExpressionOrder, assertPublicUrl, buildMerkleTree, capped, clamp, clamp01, compareChangeApplicationOrder, compareLwwStamps, compareVectorClocks, computeLwwTiebreakKey, createChunk, createContentId, deduplicatedUnion, detectFork, estimateQueryCost, evaluateCondition, exponential, fixed, formatBytes, getMostPermissiveCapability, grantActionSatisfies, 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
|
@@ -267,8 +267,38 @@ function deduplicatedUnion(results) {
|
|
|
267
267
|
return output;
|
|
268
268
|
}
|
|
269
269
|
|
|
270
|
+
// src/auth-types.ts
|
|
271
|
+
var AUTH_ACTIONS = [
|
|
272
|
+
"read",
|
|
273
|
+
"create",
|
|
274
|
+
"update",
|
|
275
|
+
"write",
|
|
276
|
+
"delete",
|
|
277
|
+
"share",
|
|
278
|
+
"admin"
|
|
279
|
+
];
|
|
280
|
+
function actionExpressionOrder(action) {
|
|
281
|
+
if (action === "create") return ["create", "write"];
|
|
282
|
+
if (action === "update" || action === "write") return ["update", "write"];
|
|
283
|
+
return [action];
|
|
284
|
+
}
|
|
285
|
+
function grantActionSatisfies(granted, checked) {
|
|
286
|
+
if (granted === checked) return true;
|
|
287
|
+
if (granted === "write") return checked === "create" || checked === "update";
|
|
288
|
+
if (granted === "update") return checked === "write";
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
270
292
|
// src/permissions.ts
|
|
271
|
-
var ALL_CAPABILITIES = [
|
|
293
|
+
var ALL_CAPABILITIES = [
|
|
294
|
+
"read",
|
|
295
|
+
"create",
|
|
296
|
+
"update",
|
|
297
|
+
"write",
|
|
298
|
+
"delete",
|
|
299
|
+
"share",
|
|
300
|
+
"admin"
|
|
301
|
+
];
|
|
272
302
|
var STANDARD_ROLES = {
|
|
273
303
|
viewer: {
|
|
274
304
|
id: "viewer",
|
|
@@ -284,7 +314,7 @@ var STANDARD_ROLES = {
|
|
|
284
314
|
}
|
|
285
315
|
};
|
|
286
316
|
function roleHasCapability(role, capability) {
|
|
287
|
-
return role.capabilities.
|
|
317
|
+
return role.capabilities.some((held) => grantActionSatisfies(held, capability));
|
|
288
318
|
}
|
|
289
319
|
function evaluateCondition(condition, context) {
|
|
290
320
|
switch (condition.type) {
|
|
@@ -306,9 +336,6 @@ function getMostPermissiveCapability(capabilities) {
|
|
|
306
336
|
return null;
|
|
307
337
|
}
|
|
308
338
|
|
|
309
|
-
// src/auth-types.ts
|
|
310
|
-
var AUTH_ACTIONS = ["read", "write", "delete", "share", "admin"];
|
|
311
|
-
|
|
312
339
|
// src/utils/math.ts
|
|
313
340
|
function clamp(value, min, max) {
|
|
314
341
|
if (value < min) return min;
|
|
@@ -408,10 +435,98 @@ function validateExternalUrl(rawUrl) {
|
|
|
408
435
|
}
|
|
409
436
|
}
|
|
410
437
|
|
|
438
|
+
// src/retry/policy.ts
|
|
439
|
+
function fixed(delayMs) {
|
|
440
|
+
return {
|
|
441
|
+
delayFor: () => delayMs
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
function exponential(baseMs, factor = 2) {
|
|
445
|
+
return {
|
|
446
|
+
delayFor: (attempt) => baseMs * factor ** (attempt - 1)
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
function capped(policy, maxDelayMs) {
|
|
450
|
+
return {
|
|
451
|
+
delayFor: (attempt) => {
|
|
452
|
+
const delay = policy.delayFor(attempt);
|
|
453
|
+
return delay === null ? null : Math.min(delay, maxDelayMs);
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function jittered(policy, ratio = 0.5, random = Math.random) {
|
|
458
|
+
return {
|
|
459
|
+
delayFor: (attempt) => {
|
|
460
|
+
const delay = policy.delayFor(attempt);
|
|
461
|
+
return delay === null ? null : delay + Math.floor(random() * delay * ratio);
|
|
462
|
+
}
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
function limitAttempts(policy, maxAttempts) {
|
|
466
|
+
return {
|
|
467
|
+
delayFor: (attempt) => attempt > maxAttempts ? null : policy.delayFor(attempt)
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// src/errors/tagged.ts
|
|
472
|
+
var TaggedError = class extends Error {
|
|
473
|
+
constructor(message, options) {
|
|
474
|
+
super(message, options);
|
|
475
|
+
this.name = new.target.name;
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
function isTagged(error, tag) {
|
|
479
|
+
return error instanceof TaggedError && error._tag === tag;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// src/async/single-flight.ts
|
|
483
|
+
function singleFlight(map, key, fn, options = {}) {
|
|
484
|
+
const existing = map.get(key);
|
|
485
|
+
if (existing) return existing;
|
|
486
|
+
const promise = fn();
|
|
487
|
+
map.set(key, promise);
|
|
488
|
+
const evict = () => {
|
|
489
|
+
if (map.get(key) === promise) map.delete(key);
|
|
490
|
+
};
|
|
491
|
+
if ((options.retain ?? "settled") === "settled") {
|
|
492
|
+
promise.then(evict, evict);
|
|
493
|
+
} else {
|
|
494
|
+
promise.then(void 0, evict);
|
|
495
|
+
}
|
|
496
|
+
return promise;
|
|
497
|
+
}
|
|
498
|
+
|
|
411
499
|
// src/lww.ts
|
|
500
|
+
import { blake3 as blake32 } from "@noble/hashes/blake3.js";
|
|
501
|
+
var LWW_TIEBREAK_KEY_VERSION = 4;
|
|
502
|
+
var US = "";
|
|
503
|
+
function canonicalize(value) {
|
|
504
|
+
if (value === null || typeof value !== "object") return value;
|
|
505
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
506
|
+
const out = {};
|
|
507
|
+
for (const key of Object.keys(value).sort()) {
|
|
508
|
+
out[key] = canonicalize(value[key]);
|
|
509
|
+
}
|
|
510
|
+
return out;
|
|
511
|
+
}
|
|
512
|
+
function toHex(bytes) {
|
|
513
|
+
let s = "";
|
|
514
|
+
for (const b of bytes) s += b.toString(16).padStart(2, "0");
|
|
515
|
+
return s;
|
|
516
|
+
}
|
|
517
|
+
function computeLwwTiebreakKey(author, propertyKey, value) {
|
|
518
|
+
const canonical = JSON.stringify(canonicalize(value === void 0 ? null : value));
|
|
519
|
+
const bytes = new TextEncoder().encode(`${author}${US}${propertyKey}${US}${canonical}`);
|
|
520
|
+
return toHex(blake32(bytes));
|
|
521
|
+
}
|
|
412
522
|
function compareLwwStamps(a, b) {
|
|
413
523
|
if (a.lamport !== b.lamport) return a.lamport - b.lamport;
|
|
414
524
|
if (a.wallTime !== b.wallTime) return a.wallTime - b.wallTime;
|
|
525
|
+
const aK = a.tiebreakKey;
|
|
526
|
+
const bK = b.tiebreakKey;
|
|
527
|
+
if (aK !== void 0 && bK !== void 0 && aK !== bK) {
|
|
528
|
+
return aK < bK ? -1 : 1;
|
|
529
|
+
}
|
|
415
530
|
return a.author < b.author ? -1 : a.author > b.author ? 1 : 0;
|
|
416
531
|
}
|
|
417
532
|
function lwwWins(incoming, existing) {
|
|
@@ -422,12 +537,19 @@ function compareChangeApplicationOrder(a, b) {
|
|
|
422
537
|
return a.author < b.author ? -1 : a.author > b.author ? 1 : 0;
|
|
423
538
|
}
|
|
424
539
|
function lwwUpdateGuardSql(input) {
|
|
425
|
-
const { table, lamportColumn, wallTimeColumn, authorColumn } = input;
|
|
540
|
+
const { table, lamportColumn, wallTimeColumn, authorColumn, tiebreakKeyColumn } = input;
|
|
541
|
+
const finalRung = tiebreakKeyColumn ? `(excluded.${tiebreakKeyColumn} IS NOT NULL
|
|
542
|
+
AND ${table}.${tiebreakKeyColumn} IS NOT NULL
|
|
543
|
+
AND excluded.${tiebreakKeyColumn} > ${table}.${tiebreakKeyColumn})
|
|
544
|
+
OR (NOT (excluded.${tiebreakKeyColumn} IS NOT NULL
|
|
545
|
+
AND ${table}.${tiebreakKeyColumn} IS NOT NULL
|
|
546
|
+
AND excluded.${tiebreakKeyColumn} <> ${table}.${tiebreakKeyColumn})
|
|
547
|
+
AND excluded.${authorColumn} > ${table}.${authorColumn})` : `excluded.${authorColumn} > ${table}.${authorColumn}`;
|
|
426
548
|
return `excluded.${lamportColumn} > ${table}.${lamportColumn}
|
|
427
549
|
OR (excluded.${lamportColumn} = ${table}.${lamportColumn}
|
|
428
550
|
AND (excluded.${wallTimeColumn} > ${table}.${wallTimeColumn}
|
|
429
551
|
OR (excluded.${wallTimeColumn} = ${table}.${wallTimeColumn}
|
|
430
|
-
AND
|
|
552
|
+
AND (${finalRung}))))`;
|
|
431
553
|
}
|
|
432
554
|
export {
|
|
433
555
|
ALL_CAPABILITIES,
|
|
@@ -436,29 +558,40 @@ export {
|
|
|
436
558
|
DEFAULT_SNAPSHOT_TRIGGERS,
|
|
437
559
|
DEFAULT_STREAMING_OPTIONS,
|
|
438
560
|
DHT_CONFIG,
|
|
561
|
+
LWW_TIEBREAK_KEY_VERSION,
|
|
439
562
|
RESOLUTION_CACHE_CONFIG,
|
|
440
563
|
STANDARD_ROLES,
|
|
441
564
|
SsrfError,
|
|
565
|
+
TaggedError,
|
|
566
|
+
actionExpressionOrder,
|
|
442
567
|
assertPublicUrl,
|
|
443
568
|
buildMerkleTree,
|
|
569
|
+
capped,
|
|
444
570
|
clamp,
|
|
445
571
|
clamp01,
|
|
446
572
|
compareChangeApplicationOrder,
|
|
447
573
|
compareLwwStamps,
|
|
448
574
|
compareVectorClocks,
|
|
575
|
+
computeLwwTiebreakKey,
|
|
449
576
|
createChunk,
|
|
450
577
|
createContentId,
|
|
451
578
|
deduplicatedUnion,
|
|
452
579
|
detectFork,
|
|
453
580
|
estimateQueryCost,
|
|
454
581
|
evaluateCondition,
|
|
582
|
+
exponential,
|
|
583
|
+
fixed,
|
|
455
584
|
formatBytes,
|
|
456
585
|
getMostPermissiveCapability,
|
|
586
|
+
grantActionSatisfies,
|
|
457
587
|
hashContent,
|
|
458
588
|
incrementVectorClock,
|
|
459
589
|
isLocationFresh,
|
|
590
|
+
isTagged,
|
|
460
591
|
isValidDID,
|
|
461
592
|
isValidProgression,
|
|
593
|
+
jittered,
|
|
594
|
+
limitAttempts,
|
|
462
595
|
lwwUpdateGuardSql,
|
|
463
596
|
lwwWins,
|
|
464
597
|
mergeStateVectors,
|
|
@@ -467,6 +600,7 @@ export {
|
|
|
467
600
|
parseDID,
|
|
468
601
|
roleHasCapability,
|
|
469
602
|
shouldCreateSnapshot,
|
|
603
|
+
singleFlight,
|
|
470
604
|
unionAggregate,
|
|
471
605
|
validateExternalUrl,
|
|
472
606
|
verifyContent,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xnetjs/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.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
|
},
|