@kubun/mutation 0.17.0 → 0.18.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/lib/access-slot-only.d.ts +3 -0
- package/lib/access-slot-only.js +23 -0
- package/lib/apply.d.ts +156 -6
- package/lib/apply.js +740 -72
- package/lib/create.d.ts +12 -2
- package/lib/create.js +29 -2
- package/lib/errors.d.ts +129 -10
- package/lib/errors.js +258 -25
- package/lib/index.d.ts +9 -5
- package/lib/index.js +6 -3
- package/lib/observation-winner.d.ts +19 -0
- package/lib/observation-winner.js +48 -0
- package/lib/operations.d.ts +15 -2
- package/lib/operations.js +21 -4
- package/lib/state-digest.d.ts +16 -0
- package/lib/state-digest.js +21 -0
- package/package.json +6 -7
package/lib/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export { HLC } from '@kubun/hlc';
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
2
|
+
export { isAccessSlotOnlyMutation } from './access-slot-only.js';
|
|
3
|
+
export { applyChangeMutation, applyMutation, applySetMutation, applyTombstoneMutation, assembleDocumentData, classifySetOutcome, classifyTombstoneOutcome, DEFAULT_MAX_DRIFT_MS, DELETE_FETCH_KEY, getTopLevelField, providerGroupDigest, resolveModelPolicy, SET_FETCH_KEY, validateFetchTokenDrift, validateSetData, winnerStateDigestOf } from './apply.js';
|
|
4
|
+
export { createChangeMutation, createRemoveMutation, createSetMutation, createTombstoneMutation } from './create.js';
|
|
5
|
+
export { DocumentIDMismatchError, FetchTokenOrderError, HLCDriftError, IncompleteProviderGroupError, InvalidAccessPermissionsError, InvalidAccessRulePatchError, InvalidDefaultAccessPermissionsError, InvalidFetchTokenError, isPermanentApplyError, KubunMutationError, MergePolicyRemoveRequiresTombstoneError, MissingFetchTokenError, MutationSchemaValidationError, OwnerChangeError, ProviderGroupChangeRejectedError, TombstoneRequiresMergePolicyError, UnsupportedMutationTypeError, WriteAccessDeniedError } from './errors.js';
|
|
6
|
+
export { selectHeldProviderWinner } from './observation-winner.js';
|
|
5
7
|
export { convertPatchInput, createMutationOperations } from './operations.js';
|
|
8
|
+
export { DELETED_STATE_DIGEST, stateDigest } from './state-digest.js';
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SetDocumentMutation, TombstoneDocumentMutation } from '@kubun/protocol';
|
|
2
|
+
import { type DocumentValidator, type ObservedProviderValue } from './apply.js';
|
|
3
|
+
export type HeldProviderMutation = (SetDocumentMutation | TombstoneDocumentMutation) & {
|
|
4
|
+
sourceFetchHLC: string;
|
|
5
|
+
};
|
|
6
|
+
export type HeldProviderWinner = {
|
|
7
|
+
mutation: HeldProviderMutation;
|
|
8
|
+
token: string;
|
|
9
|
+
digest: string;
|
|
10
|
+
present: boolean;
|
|
11
|
+
group: Record<string, unknown>;
|
|
12
|
+
};
|
|
13
|
+
export type SelectHeldProviderWinnerParams = {
|
|
14
|
+
mutations: ReadonlyArray<HeldProviderMutation>;
|
|
15
|
+
observed: ObservedProviderValue;
|
|
16
|
+
fields: ReadonlyArray<string>;
|
|
17
|
+
validator: DocumentValidator;
|
|
18
|
+
};
|
|
19
|
+
export declare function selectHeldProviderWinner(params: SelectHeldProviderWinnerParams): HeldProviderWinner | null;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { providerGroupDigest, validateSetData } from './apply.js';
|
|
2
|
+
import { DELETED_STATE_DIGEST, stateDigest } from './state-digest.js';
|
|
3
|
+
function candidate(mutation, fields, validator) {
|
|
4
|
+
const token = mutation.sourceFetchHLC;
|
|
5
|
+
if (mutation.typ === 'tombstone') {
|
|
6
|
+
return {
|
|
7
|
+
mutation,
|
|
8
|
+
token,
|
|
9
|
+
digest: DELETED_STATE_DIGEST,
|
|
10
|
+
present: false,
|
|
11
|
+
group: {}
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
const group = {};
|
|
15
|
+
const data = validateSetData(validator, mutation.data, mutation.sub);
|
|
16
|
+
for (const field of fields){
|
|
17
|
+
if (field in data) group[field] = data[field];
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
mutation,
|
|
21
|
+
token,
|
|
22
|
+
digest: stateDigest({
|
|
23
|
+
state: 'live',
|
|
24
|
+
group
|
|
25
|
+
}),
|
|
26
|
+
present: true,
|
|
27
|
+
group
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function comparesAfter(left, right) {
|
|
31
|
+
if (left.token !== right.token) return left.token > right.token;
|
|
32
|
+
if (left.present !== right.present) return !left.present;
|
|
33
|
+
return left.digest > right.digest;
|
|
34
|
+
}
|
|
35
|
+
export function selectHeldProviderWinner(params) {
|
|
36
|
+
const { mutations, observed, fields, validator } = params;
|
|
37
|
+
let base = null;
|
|
38
|
+
let qualifying = null;
|
|
39
|
+
for (const mutation of mutations){
|
|
40
|
+
const current = candidate(mutation, fields, validator);
|
|
41
|
+
if (base === null || comparesAfter(current, base)) base = current;
|
|
42
|
+
const matches = current.present === observed.present && (current.present ? providerGroupDigest(current.group) === observed.valueDigest : observed.valueDigest === null);
|
|
43
|
+
if (current.token >= observed.fetchHLC || matches) {
|
|
44
|
+
if (qualifying === null || comparesAfter(current, qualifying)) qualifying = current;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return qualifying ?? base;
|
|
48
|
+
}
|
package/lib/operations.d.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import type { PatchOperation } from '@kubun/graphql';
|
|
2
1
|
import type { HLC } from '@kubun/hlc';
|
|
3
|
-
import type { ChangeDocumentMutation, DocumentData, SetDocumentMutation } from '@kubun/protocol';
|
|
2
|
+
import type { ChangeDocumentMutation, DocumentData, PatchOperation, SetDocumentMutation, TombstoneDocumentMutation } from '@kubun/protocol';
|
|
4
3
|
import type { GetRandomValues } from '@sozai/runtime';
|
|
5
4
|
export type CreateMutationOperationsParams<T> = {
|
|
6
5
|
issuer: string;
|
|
@@ -10,6 +9,7 @@ export type CreateMutationOperationsParams<T> = {
|
|
|
10
9
|
cap?: string | Array<string>;
|
|
11
10
|
processSetMutation(mutation: SetDocumentMutation, transactionID?: string | null): Promise<T>;
|
|
12
11
|
processChangeMutation(mutation: ChangeDocumentMutation, transactionID?: string | null): Promise<T>;
|
|
12
|
+
processTombstoneMutation?(mutation: TombstoneDocumentMutation, transactionID?: string | null): Promise<T>;
|
|
13
13
|
};
|
|
14
14
|
export type CreateDocumentParams = {
|
|
15
15
|
modelID: string;
|
|
@@ -31,6 +31,7 @@ export type SetDocumentParams = {
|
|
|
31
31
|
* Omitted means owner = the ops-level owner (or the signer when unset).
|
|
32
32
|
*/
|
|
33
33
|
owner?: string;
|
|
34
|
+
sourceFetchHLC?: string;
|
|
34
35
|
};
|
|
35
36
|
export type UpdateDocumentParams = {
|
|
36
37
|
docID: string;
|
|
@@ -41,11 +42,23 @@ export type RemoveDocumentParams = {
|
|
|
41
42
|
docID: string;
|
|
42
43
|
transactionID?: string | null;
|
|
43
44
|
};
|
|
45
|
+
export type TombstoneDocumentParams = {
|
|
46
|
+
modelID: string;
|
|
47
|
+
unique: Uint8Array;
|
|
48
|
+
sourceFetchHLC: string;
|
|
49
|
+
transactionID?: string | null;
|
|
50
|
+
/**
|
|
51
|
+
* Per-call document owner, overriding the ops-level owner for this write only.
|
|
52
|
+
* Omitted means owner = the ops-level owner (or the signer when unset).
|
|
53
|
+
*/
|
|
54
|
+
owner?: string;
|
|
55
|
+
};
|
|
44
56
|
export type MutationOperations<T> = {
|
|
45
57
|
createDocument(params: CreateDocumentParams): Promise<T>;
|
|
46
58
|
setDocument(params: SetDocumentParams): Promise<T>;
|
|
47
59
|
updateDocument(params: UpdateDocumentParams): Promise<T>;
|
|
48
60
|
removeDocument(params: RemoveDocumentParams): Promise<T>;
|
|
61
|
+
tombstoneDocument(params: TombstoneDocumentParams): Promise<T>;
|
|
49
62
|
};
|
|
50
63
|
export declare function createMutationOperations<T>(params: CreateMutationOperationsParams<T>): MutationOperations<T>;
|
|
51
64
|
export declare function convertPatchInput(patch: Array<Record<string, unknown>>): Array<PatchOperation>;
|
package/lib/operations.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { createChangeMutation, createRemoveMutation, createSetMutation } from './create.js';
|
|
1
|
+
import { createChangeMutation, createRemoveMutation, createSetMutation, createTombstoneMutation } from './create.js';
|
|
2
2
|
export function createMutationOperations(params) {
|
|
3
|
-
const { issuer, hlc, getRandomValues, owner, cap, processSetMutation, processChangeMutation } = params;
|
|
3
|
+
const { issuer, hlc, getRandomValues, owner, cap, processSetMutation, processChangeMutation, processTombstoneMutation } = params;
|
|
4
4
|
return {
|
|
5
5
|
async createDocument (params) {
|
|
6
6
|
const { modelID, data, transactionID } = params;
|
|
@@ -17,7 +17,7 @@ export function createMutationOperations(params) {
|
|
|
17
17
|
return await processSetMutation(mutation, transactionID);
|
|
18
18
|
},
|
|
19
19
|
async setDocument (params) {
|
|
20
|
-
const { modelID, unique, data, transactionID } = params;
|
|
20
|
+
const { modelID, unique, data, transactionID, sourceFetchHLC } = params;
|
|
21
21
|
const mutation = await createSetMutation({
|
|
22
22
|
issuer,
|
|
23
23
|
hlc,
|
|
@@ -25,7 +25,8 @@ export function createMutationOperations(params) {
|
|
|
25
25
|
data,
|
|
26
26
|
unique,
|
|
27
27
|
owner: params.owner ?? owner,
|
|
28
|
-
cap
|
|
28
|
+
cap,
|
|
29
|
+
sourceFetchHLC
|
|
29
30
|
});
|
|
30
31
|
return await processSetMutation(mutation, transactionID);
|
|
31
32
|
},
|
|
@@ -47,6 +48,22 @@ export function createMutationOperations(params) {
|
|
|
47
48
|
cap
|
|
48
49
|
});
|
|
49
50
|
return await processChangeMutation(mutation, transactionID);
|
|
51
|
+
},
|
|
52
|
+
async tombstoneDocument (params) {
|
|
53
|
+
if (processTombstoneMutation == null) {
|
|
54
|
+
throw new Error('tombstoneDocument requires processTombstoneMutation');
|
|
55
|
+
}
|
|
56
|
+
const { modelID, unique, sourceFetchHLC, transactionID } = params;
|
|
57
|
+
const mutation = await createTombstoneMutation({
|
|
58
|
+
issuer,
|
|
59
|
+
hlc,
|
|
60
|
+
modelID,
|
|
61
|
+
unique,
|
|
62
|
+
owner: params.owner ?? owner,
|
|
63
|
+
cap,
|
|
64
|
+
sourceFetchHLC
|
|
65
|
+
});
|
|
66
|
+
return await processTombstoneMutation(mutation, transactionID);
|
|
50
67
|
}
|
|
51
68
|
};
|
|
52
69
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** The candidate's state, folded into one BLAKE3 digest with a liveness marker. */
|
|
2
|
+
export type StateDigestInput = {
|
|
3
|
+
state: 'live';
|
|
4
|
+
group: Record<string, unknown>;
|
|
5
|
+
} | {
|
|
6
|
+
state: 'deleted';
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* The candidate's state digest as a base64url (no padding) string. One
|
|
10
|
+
* definition serves the equal-token tiebreak, the `$setFetch`/`/fetch`
|
|
11
|
+
* registers, the watch watermarks, and the seam's candidate/winner digests.
|
|
12
|
+
* `group` excludes the fetch token, the digest itself, and every
|
|
13
|
+
* local-annotation field.
|
|
14
|
+
*/
|
|
15
|
+
export declare function stateDigest(input: StateDigestInput): string;
|
|
16
|
+
export declare const DELETED_STATE_DIGEST: string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { digestJSON } from '@kubun/id';
|
|
2
|
+
import { toB64U } from '@sozai/codec';
|
|
3
|
+
/**
|
|
4
|
+
* The candidate's state digest as a base64url (no padding) string. One
|
|
5
|
+
* definition serves the equal-token tiebreak, the `$setFetch`/`/fetch`
|
|
6
|
+
* registers, the watch watermarks, and the seam's candidate/winner digests.
|
|
7
|
+
* `group` excludes the fetch token, the digest itself, and every
|
|
8
|
+
* local-annotation field.
|
|
9
|
+
*/ export function stateDigest(input) {
|
|
10
|
+
const canonical = input.state === 'live' ? {
|
|
11
|
+
state: 'live',
|
|
12
|
+
group: input.group
|
|
13
|
+
} : {
|
|
14
|
+
state: 'deleted'
|
|
15
|
+
};
|
|
16
|
+
const bytes = digestJSON(canonical);
|
|
17
|
+
return toB64U(bytes);
|
|
18
|
+
}
|
|
19
|
+
export const DELETED_STATE_DIGEST = stateDigest({
|
|
20
|
+
state: 'deleted'
|
|
21
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/mutation",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"keywords": [],
|
|
5
5
|
"license": "see LICENSE.md",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -15,16 +15,15 @@
|
|
|
15
15
|
"LICENSE.md"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@kubun/hlc": "^0.
|
|
19
|
-
"@kubun/id": "^0.
|
|
20
|
-
"@sozai/codec": "^0.4.
|
|
18
|
+
"@kubun/hlc": "^0.18.0",
|
|
19
|
+
"@kubun/id": "^0.18.0",
|
|
20
|
+
"@sozai/codec": "^0.4.3",
|
|
21
21
|
"@sozai/patch": "^0.1.3",
|
|
22
22
|
"@sozai/runtime": "^0.1.0",
|
|
23
|
-
"@sozai/schema": "^0.1.
|
|
23
|
+
"@sozai/schema": "^0.1.4"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@kubun/
|
|
27
|
-
"@kubun/protocol": "^0.17.0"
|
|
26
|
+
"@kubun/protocol": "^0.18.0"
|
|
28
27
|
},
|
|
29
28
|
"publishConfig": {
|
|
30
29
|
"access": "public"
|