@kubun/mutation 0.16.0 → 0.17.1
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/apply.d.ts +139 -4
- package/lib/apply.js +541 -46
- package/lib/create.d.ts +12 -2
- package/lib/create.js +29 -2
- package/lib/errors.d.ts +103 -0
- package/lib/errors.js +165 -1
- package/lib/index.d.ts +6 -5
- package/lib/index.js +4 -3
- 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/create.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import type { PatchOperation } from '@kubun/graphql';
|
|
2
1
|
import { HLC } from '@kubun/hlc';
|
|
3
2
|
import { DocumentID, type DocumentModelID } from '@kubun/id';
|
|
4
|
-
import type { ChangeDocumentMutation, DocumentData, SetDocumentMutation } from '@kubun/protocol';
|
|
3
|
+
import type { ChangeDocumentMutation, DocumentData, PatchOperation, SetDocumentMutation, TombstoneDocumentMutation } from '@kubun/protocol';
|
|
5
4
|
export type CreateSetMutationParams<Data extends DocumentData = DocumentData> = {
|
|
6
5
|
data: Data;
|
|
7
6
|
hlc: HLC;
|
|
@@ -10,6 +9,7 @@ export type CreateSetMutationParams<Data extends DocumentData = DocumentData> =
|
|
|
10
9
|
owner?: string;
|
|
11
10
|
unique: Uint8Array;
|
|
12
11
|
cap?: string | Array<string>;
|
|
12
|
+
sourceFetchHLC?: string;
|
|
13
13
|
};
|
|
14
14
|
export declare function createSetMutation<Data extends DocumentData = DocumentData>(params: CreateSetMutationParams<Data>): Promise<SetDocumentMutation>;
|
|
15
15
|
export type CreateChangeMutationParams = {
|
|
@@ -27,3 +27,13 @@ export type CreateRemoveMutationParams = {
|
|
|
27
27
|
cap?: string | Array<string>;
|
|
28
28
|
};
|
|
29
29
|
export declare function createRemoveMutation(params: CreateRemoveMutationParams): ChangeDocumentMutation;
|
|
30
|
+
export type CreateTombstoneMutationParams = {
|
|
31
|
+
hlc: HLC;
|
|
32
|
+
issuer: string;
|
|
33
|
+
modelID: DocumentModelID | string;
|
|
34
|
+
owner?: string;
|
|
35
|
+
unique: Uint8Array;
|
|
36
|
+
sourceFetchHLC: string;
|
|
37
|
+
cap?: string | Array<string>;
|
|
38
|
+
};
|
|
39
|
+
export declare function createTombstoneMutation(params: CreateTombstoneMutationParams): Promise<TombstoneDocumentMutation>;
|
package/lib/create.js
CHANGED
|
@@ -2,7 +2,7 @@ import { HLC } from '@kubun/hlc';
|
|
|
2
2
|
import { DocumentID } from '@kubun/id';
|
|
3
3
|
import { isValidAccessPermissions } from '@kubun/protocol';
|
|
4
4
|
import { toB64 } from '@sozai/codec';
|
|
5
|
-
import { InvalidAccessPermissionsError } from './errors.js';
|
|
5
|
+
import { FetchTokenOrderError, InvalidAccessPermissionsError } from './errors.js';
|
|
6
6
|
const ACCESS_PERMISSIONS_FIELD = 'accessPermissions';
|
|
7
7
|
const ACCESS_PERMISSIONS_PATH = `/${ACCESS_PERMISSIONS_FIELD}`;
|
|
8
8
|
/**
|
|
@@ -30,16 +30,23 @@ export async function createSetMutation(params) {
|
|
|
30
30
|
const issuer = params.issuer;
|
|
31
31
|
const owner = params.owner ?? issuer;
|
|
32
32
|
const docID = await DocumentID.create(params.modelID, owner, params.unique);
|
|
33
|
+
const hlc = HLC.serialize(params.hlc.now());
|
|
34
|
+
if (params.sourceFetchHLC != null && hlc < params.sourceFetchHLC) {
|
|
35
|
+
throw new FetchTokenOrderError();
|
|
36
|
+
}
|
|
33
37
|
return {
|
|
34
38
|
typ: 'set',
|
|
35
39
|
iss: issuer,
|
|
36
40
|
aud: owner,
|
|
37
41
|
sub: docID.toString(),
|
|
38
42
|
data: params.data,
|
|
39
|
-
hlc
|
|
43
|
+
hlc,
|
|
40
44
|
unq: toB64(params.unique),
|
|
41
45
|
...params.cap != null ? {
|
|
42
46
|
cap: params.cap
|
|
47
|
+
} : {},
|
|
48
|
+
...params.sourceFetchHLC != null ? {
|
|
49
|
+
sourceFetchHLC: params.sourceFetchHLC
|
|
43
50
|
} : {}
|
|
44
51
|
};
|
|
45
52
|
}
|
|
@@ -74,3 +81,23 @@ export function createRemoveMutation(params) {
|
|
|
74
81
|
} : {}
|
|
75
82
|
};
|
|
76
83
|
}
|
|
84
|
+
export async function createTombstoneMutation(params) {
|
|
85
|
+
const owner = params.owner ?? params.issuer;
|
|
86
|
+
const docID = await DocumentID.create(params.modelID, owner, params.unique);
|
|
87
|
+
const hlc = HLC.serialize(params.hlc.now());
|
|
88
|
+
if (hlc < params.sourceFetchHLC) {
|
|
89
|
+
throw new FetchTokenOrderError();
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
typ: 'tombstone',
|
|
93
|
+
iss: params.issuer,
|
|
94
|
+
aud: owner,
|
|
95
|
+
sub: docID.toString(),
|
|
96
|
+
unq: toB64(params.unique),
|
|
97
|
+
hlc,
|
|
98
|
+
sourceFetchHLC: params.sourceFetchHLC,
|
|
99
|
+
...params.cap != null ? {
|
|
100
|
+
cap: params.cap
|
|
101
|
+
} : {}
|
|
102
|
+
};
|
|
103
|
+
}
|
package/lib/errors.d.ts
CHANGED
|
@@ -39,6 +39,109 @@ export declare class HLCDriftError extends Error {
|
|
|
39
39
|
get driftMS(): number;
|
|
40
40
|
get maxDriftMS(): number;
|
|
41
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Thrown when a fetch token cannot be parsed as a valid HLC timestamp. Detects
|
|
44
|
+
* malformed tokens or NaN values that would slip past `HLC.parse` but fail
|
|
45
|
+
* `HLC.tryParse` validation. Tagged `KB16` so the RPC handler can translate the
|
|
46
|
+
* throw into a structured wire response.
|
|
47
|
+
*/
|
|
48
|
+
export declare class InvalidFetchTokenError extends Error {
|
|
49
|
+
#private;
|
|
50
|
+
constructor(token: string);
|
|
51
|
+
get code(): 'KB16';
|
|
52
|
+
get token(): string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Thrown when a mutation's minted HLC timestamp predates the supplied fetch
|
|
56
|
+
* token (sourceFetchHLC). Since HLC is a fixed property of a signed mutation,
|
|
57
|
+
* this reject is permanent — resending cannot move the HLC forward. Tagged
|
|
58
|
+
* `KB17` so the RPC handler can translate the throw into a structured wire
|
|
59
|
+
* response.
|
|
60
|
+
*/
|
|
61
|
+
export declare class FetchTokenOrderError extends Error {
|
|
62
|
+
constructor();
|
|
63
|
+
get code(): 'KB17';
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Thrown by the set apply path when a merge-policy SET omits a schema-required
|
|
67
|
+
* field of its model's provider group. A merge-policy SET must carry the
|
|
68
|
+
* complete group so a winning higher-key SET can atomically replace it (an
|
|
69
|
+
* omitted OPTIONAL group field is legitimate — it clears that field's stored
|
|
70
|
+
* value instead of throwing). Tagged `KB18` so the RPC handler can translate
|
|
71
|
+
* the throw into a structured wire response.
|
|
72
|
+
*/
|
|
73
|
+
export declare class IncompleteProviderGroupError extends Error {
|
|
74
|
+
#private;
|
|
75
|
+
constructor(docID: string, field: string);
|
|
76
|
+
get code(): 'KB18';
|
|
77
|
+
get docID(): string;
|
|
78
|
+
get field(): string;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Thrown by the tombstone apply path when the target document's model
|
|
82
|
+
* declares no `providerMergePolicy`. A tombstone advances the fetch-order
|
|
83
|
+
* `/fetch` liveness register, which only a merge-policy document's `assemble`
|
|
84
|
+
* consults — an ordinary model's liveness is governed by the plain `$set`/`/`
|
|
85
|
+
* HLC registers and deletes via the `/`→null CHANGE path instead. Tagged
|
|
86
|
+
* `KB19` so the RPC handler can translate the throw into a structured wire
|
|
87
|
+
* response.
|
|
88
|
+
*/
|
|
89
|
+
export declare class TombstoneRequiresMergePolicyError extends Error {
|
|
90
|
+
#private;
|
|
91
|
+
constructor(docID: string);
|
|
92
|
+
get code(): 'KB19';
|
|
93
|
+
get docID(): string;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Thrown by the change apply path when a CHANGE mutation targets a
|
|
97
|
+
* merge-policy model's provider-authoritative group. The group is single
|
|
98
|
+
* authority — every write to it is a fetch-token-ordered provider projection
|
|
99
|
+
* (a complete-group SET or a tombstone) — so a local HLC-ordered CHANGE
|
|
100
|
+
* against it would mix two incompatible orderings on the same fields. A
|
|
101
|
+
* CHANGE to a field outside the group is unaffected. Tagged `KB20` so the RPC
|
|
102
|
+
* handler can translate the throw into a structured wire response.
|
|
103
|
+
*/
|
|
104
|
+
export declare class ProviderGroupChangeRejectedError extends Error {
|
|
105
|
+
#private;
|
|
106
|
+
constructor(docID: string, field: string);
|
|
107
|
+
get code(): 'KB20';
|
|
108
|
+
get docID(): string;
|
|
109
|
+
get field(): string;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Thrown by the set and tombstone apply paths when a merge-policy model's
|
|
113
|
+
* single-authority write carries no `sourceFetchHLC`. No conforming producer
|
|
114
|
+
* emits a tokenless merge-policy write — `createSetMutation`/
|
|
115
|
+
* `createTombstoneMutation` mint one, and the tombstone wire schema requires
|
|
116
|
+
* it — so this is a defensive gate: it keeps the incoming token always
|
|
117
|
+
* present by the time a merge-policy candidate reaches classification. Tagged
|
|
118
|
+
* `KB21` so the RPC handler can translate the throw into a structured wire
|
|
119
|
+
* response.
|
|
120
|
+
*/
|
|
121
|
+
export declare class MissingFetchTokenError extends Error {
|
|
122
|
+
#private;
|
|
123
|
+
constructor(docID: string);
|
|
124
|
+
get code(): 'KB21';
|
|
125
|
+
get docID(): string;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Thrown by the change apply path when a generic `/`→null delete CHANGE targets
|
|
129
|
+
* a merge-policy model. A merge-policy document's liveness lives in the
|
|
130
|
+
* fetch-order registers (`$setFetch`/`/fetch`), which a delete CHANGE never
|
|
131
|
+
* advances — it would null the visible data while leaving the live register
|
|
132
|
+
* standing, so a later provider SET or any outside-group CHANGE would resurrect
|
|
133
|
+
* the document with stale group data. Such a deletion must flow through the
|
|
134
|
+
* fetch-token tombstone path instead, which carries the token that advances
|
|
135
|
+
* `/fetch`. The rejection is deterministic and permanent — the same mutation
|
|
136
|
+
* always fails. Tagged `KB22` so the RPC handler can translate the throw into a
|
|
137
|
+
* structured wire response.
|
|
138
|
+
*/
|
|
139
|
+
export declare class MergePolicyRemoveRequiresTombstoneError extends Error {
|
|
140
|
+
#private;
|
|
141
|
+
constructor(docID: string);
|
|
142
|
+
get code(): 'KB22';
|
|
143
|
+
get docID(): string;
|
|
144
|
+
}
|
|
42
145
|
/**
|
|
43
146
|
* Thrown by the set apply path when a mutation would reassign an existing
|
|
44
147
|
* document's owner. Ownership is immutable once a document exists, so this is a
|
package/lib/errors.js
CHANGED
|
@@ -64,6 +64,163 @@
|
|
|
64
64
|
return this.#maxDriftMS;
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Thrown when a fetch token cannot be parsed as a valid HLC timestamp. Detects
|
|
69
|
+
* malformed tokens or NaN values that would slip past `HLC.parse` but fail
|
|
70
|
+
* `HLC.tryParse` validation. Tagged `KB16` so the RPC handler can translate the
|
|
71
|
+
* throw into a structured wire response.
|
|
72
|
+
*/ export class InvalidFetchTokenError extends Error {
|
|
73
|
+
#token;
|
|
74
|
+
constructor(token){
|
|
75
|
+
super(`Invalid fetch token: malformed HLC timestamp '${token}'`);
|
|
76
|
+
this.name = 'InvalidFetchTokenError';
|
|
77
|
+
this.#token = token;
|
|
78
|
+
}
|
|
79
|
+
get code() {
|
|
80
|
+
return 'KB16';
|
|
81
|
+
}
|
|
82
|
+
get token() {
|
|
83
|
+
return this.#token;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Thrown when a mutation's minted HLC timestamp predates the supplied fetch
|
|
88
|
+
* token (sourceFetchHLC). Since HLC is a fixed property of a signed mutation,
|
|
89
|
+
* this reject is permanent — resending cannot move the HLC forward. Tagged
|
|
90
|
+
* `KB17` so the RPC handler can translate the throw into a structured wire
|
|
91
|
+
* response.
|
|
92
|
+
*/ export class FetchTokenOrderError extends Error {
|
|
93
|
+
constructor(){
|
|
94
|
+
super('Fetch token ordering violated: mutation HLC must be >= sourceFetchHLC');
|
|
95
|
+
this.name = 'FetchTokenOrderError';
|
|
96
|
+
}
|
|
97
|
+
get code() {
|
|
98
|
+
return 'KB17';
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Thrown by the set apply path when a merge-policy SET omits a schema-required
|
|
103
|
+
* field of its model's provider group. A merge-policy SET must carry the
|
|
104
|
+
* complete group so a winning higher-key SET can atomically replace it (an
|
|
105
|
+
* omitted OPTIONAL group field is legitimate — it clears that field's stored
|
|
106
|
+
* value instead of throwing). Tagged `KB18` so the RPC handler can translate
|
|
107
|
+
* the throw into a structured wire response.
|
|
108
|
+
*/ export class IncompleteProviderGroupError extends Error {
|
|
109
|
+
#docID;
|
|
110
|
+
#field;
|
|
111
|
+
constructor(docID, field){
|
|
112
|
+
super(`Incomplete provider group: document ${docID} SET omits required field '${field}'`);
|
|
113
|
+
this.name = 'IncompleteProviderGroupError';
|
|
114
|
+
this.#docID = docID;
|
|
115
|
+
this.#field = field;
|
|
116
|
+
}
|
|
117
|
+
get code() {
|
|
118
|
+
return 'KB18';
|
|
119
|
+
}
|
|
120
|
+
get docID() {
|
|
121
|
+
return this.#docID;
|
|
122
|
+
}
|
|
123
|
+
get field() {
|
|
124
|
+
return this.#field;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Thrown by the tombstone apply path when the target document's model
|
|
129
|
+
* declares no `providerMergePolicy`. A tombstone advances the fetch-order
|
|
130
|
+
* `/fetch` liveness register, which only a merge-policy document's `assemble`
|
|
131
|
+
* consults — an ordinary model's liveness is governed by the plain `$set`/`/`
|
|
132
|
+
* HLC registers and deletes via the `/`→null CHANGE path instead. Tagged
|
|
133
|
+
* `KB19` so the RPC handler can translate the throw into a structured wire
|
|
134
|
+
* response.
|
|
135
|
+
*/ export class TombstoneRequiresMergePolicyError extends Error {
|
|
136
|
+
#docID;
|
|
137
|
+
constructor(docID){
|
|
138
|
+
super(`Tombstone requires a provider-merge-policy model: document ${docID}`);
|
|
139
|
+
this.name = 'TombstoneRequiresMergePolicyError';
|
|
140
|
+
this.#docID = docID;
|
|
141
|
+
}
|
|
142
|
+
get code() {
|
|
143
|
+
return 'KB19';
|
|
144
|
+
}
|
|
145
|
+
get docID() {
|
|
146
|
+
return this.#docID;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Thrown by the change apply path when a CHANGE mutation targets a
|
|
151
|
+
* merge-policy model's provider-authoritative group. The group is single
|
|
152
|
+
* authority — every write to it is a fetch-token-ordered provider projection
|
|
153
|
+
* (a complete-group SET or a tombstone) — so a local HLC-ordered CHANGE
|
|
154
|
+
* against it would mix two incompatible orderings on the same fields. A
|
|
155
|
+
* CHANGE to a field outside the group is unaffected. Tagged `KB20` so the RPC
|
|
156
|
+
* handler can translate the throw into a structured wire response.
|
|
157
|
+
*/ export class ProviderGroupChangeRejectedError extends Error {
|
|
158
|
+
#docID;
|
|
159
|
+
#field;
|
|
160
|
+
constructor(docID, field){
|
|
161
|
+
super(`Provider group CHANGE rejected: document ${docID} CHANGE targets provider group field '${field}'`);
|
|
162
|
+
this.name = 'ProviderGroupChangeRejectedError';
|
|
163
|
+
this.#docID = docID;
|
|
164
|
+
this.#field = field;
|
|
165
|
+
}
|
|
166
|
+
get code() {
|
|
167
|
+
return 'KB20';
|
|
168
|
+
}
|
|
169
|
+
get docID() {
|
|
170
|
+
return this.#docID;
|
|
171
|
+
}
|
|
172
|
+
get field() {
|
|
173
|
+
return this.#field;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Thrown by the set and tombstone apply paths when a merge-policy model's
|
|
178
|
+
* single-authority write carries no `sourceFetchHLC`. No conforming producer
|
|
179
|
+
* emits a tokenless merge-policy write — `createSetMutation`/
|
|
180
|
+
* `createTombstoneMutation` mint one, and the tombstone wire schema requires
|
|
181
|
+
* it — so this is a defensive gate: it keeps the incoming token always
|
|
182
|
+
* present by the time a merge-policy candidate reaches classification. Tagged
|
|
183
|
+
* `KB21` so the RPC handler can translate the throw into a structured wire
|
|
184
|
+
* response.
|
|
185
|
+
*/ export class MissingFetchTokenError extends Error {
|
|
186
|
+
#docID;
|
|
187
|
+
constructor(docID){
|
|
188
|
+
super(`Missing fetch token: document ${docID} merge-policy write carries no sourceFetchHLC`);
|
|
189
|
+
this.name = 'MissingFetchTokenError';
|
|
190
|
+
this.#docID = docID;
|
|
191
|
+
}
|
|
192
|
+
get code() {
|
|
193
|
+
return 'KB21';
|
|
194
|
+
}
|
|
195
|
+
get docID() {
|
|
196
|
+
return this.#docID;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Thrown by the change apply path when a generic `/`→null delete CHANGE targets
|
|
201
|
+
* a merge-policy model. A merge-policy document's liveness lives in the
|
|
202
|
+
* fetch-order registers (`$setFetch`/`/fetch`), which a delete CHANGE never
|
|
203
|
+
* advances — it would null the visible data while leaving the live register
|
|
204
|
+
* standing, so a later provider SET or any outside-group CHANGE would resurrect
|
|
205
|
+
* the document with stale group data. Such a deletion must flow through the
|
|
206
|
+
* fetch-token tombstone path instead, which carries the token that advances
|
|
207
|
+
* `/fetch`. The rejection is deterministic and permanent — the same mutation
|
|
208
|
+
* always fails. Tagged `KB22` so the RPC handler can translate the throw into a
|
|
209
|
+
* structured wire response.
|
|
210
|
+
*/ export class MergePolicyRemoveRequiresTombstoneError extends Error {
|
|
211
|
+
#docID;
|
|
212
|
+
constructor(docID){
|
|
213
|
+
super(`Merge-policy remove requires a tombstone: document ${docID} must be deleted via the fetch-token tombstone path`);
|
|
214
|
+
this.name = 'MergePolicyRemoveRequiresTombstoneError';
|
|
215
|
+
this.#docID = docID;
|
|
216
|
+
}
|
|
217
|
+
get code() {
|
|
218
|
+
return 'KB22';
|
|
219
|
+
}
|
|
220
|
+
get docID() {
|
|
221
|
+
return this.#docID;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
67
224
|
/**
|
|
68
225
|
* Thrown by the set apply path when a mutation would reassign an existing
|
|
69
226
|
* document's owner. Ownership is immutable once a document exists, so this is a
|
|
@@ -142,7 +299,14 @@ const PERMANENT_APPLY_ERROR_CODES = new Set([
|
|
|
142
299
|
'KB10',
|
|
143
300
|
'KB11',
|
|
144
301
|
'KB12',
|
|
145
|
-
'KB13'
|
|
302
|
+
'KB13',
|
|
303
|
+
'KB16',
|
|
304
|
+
'KB17',
|
|
305
|
+
'KB18',
|
|
306
|
+
'KB19',
|
|
307
|
+
'KB20',
|
|
308
|
+
'KB21',
|
|
309
|
+
'KB22'
|
|
146
310
|
]);
|
|
147
311
|
/**
|
|
148
312
|
* True when `err` carries a protocol error code identifying a deterministically
|
package/lib/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export { HLC, type HLCTimestamp } from '@kubun/hlc';
|
|
2
|
-
export type { DocumentValidator, MutationContext, MutationDB, PostStateGate, ValidatorsRecord, } from './apply.js';
|
|
3
|
-
export { applyChangeMutation, applyMutation, applySetMutation, DEFAULT_MAX_DRIFT_MS, getTopLevelField, } from './apply.js';
|
|
4
|
-
export type { CreateChangeMutationParams, CreateRemoveMutationParams, CreateSetMutationParams, } from './create.js';
|
|
5
|
-
export { createChangeMutation, createRemoveMutation, createSetMutation } from './create.js';
|
|
6
|
-
export { DocumentIDMismatchError, HLCDriftError, InvalidAccessPermissionsError, isPermanentApplyError, MutationSchemaValidationError, OwnerChangeError, UnsupportedMutationTypeError, WriteAccessDeniedError, } from './errors.js';
|
|
2
|
+
export type { DocumentValidator, FetchFrontier, MergeOutcome, MutationApplyResult, MutationContext, MutationDB, PostStateGate, ValidatorsRecord, } from './apply.js';
|
|
3
|
+
export { applyChangeMutation, applyMutation, applySetMutation, applyTombstoneMutation, classifySetOutcome, classifyTombstoneOutcome, DEFAULT_MAX_DRIFT_MS, DELETE_FETCH_KEY, getTopLevelField, resolveModelPolicy, SET_FETCH_KEY, validateFetchTokenDrift, winnerStateDigestOf, } from './apply.js';
|
|
4
|
+
export type { CreateChangeMutationParams, CreateRemoveMutationParams, CreateSetMutationParams, CreateTombstoneMutationParams, } from './create.js';
|
|
5
|
+
export { createChangeMutation, createRemoveMutation, createSetMutation, createTombstoneMutation, } from './create.js';
|
|
6
|
+
export { DocumentIDMismatchError, FetchTokenOrderError, HLCDriftError, IncompleteProviderGroupError, InvalidAccessPermissionsError, InvalidFetchTokenError, isPermanentApplyError, MergePolicyRemoveRequiresTombstoneError, MissingFetchTokenError, MutationSchemaValidationError, OwnerChangeError, ProviderGroupChangeRejectedError, TombstoneRequiresMergePolicyError, UnsupportedMutationTypeError, WriteAccessDeniedError, } from './errors.js';
|
|
7
7
|
export type { CreateDocumentParams, CreateMutationOperationsParams, MutationOperations, RemoveDocumentParams, SetDocumentParams, UpdateDocumentParams, } from './operations.js';
|
|
8
8
|
export { convertPatchInput, createMutationOperations } from './operations.js';
|
|
9
|
+
export { DELETED_STATE_DIGEST, type StateDigestInput, stateDigest } from './state-digest.js';
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { HLC } from '@kubun/hlc';
|
|
2
|
-
export { applyChangeMutation, applyMutation, applySetMutation, DEFAULT_MAX_DRIFT_MS, getTopLevelField } from './apply.js';
|
|
3
|
-
export { createChangeMutation, createRemoveMutation, createSetMutation } from './create.js';
|
|
4
|
-
export { DocumentIDMismatchError, HLCDriftError, InvalidAccessPermissionsError, isPermanentApplyError, MutationSchemaValidationError, OwnerChangeError, UnsupportedMutationTypeError, WriteAccessDeniedError } from './errors.js';
|
|
2
|
+
export { applyChangeMutation, applyMutation, applySetMutation, applyTombstoneMutation, classifySetOutcome, classifyTombstoneOutcome, DEFAULT_MAX_DRIFT_MS, DELETE_FETCH_KEY, getTopLevelField, resolveModelPolicy, SET_FETCH_KEY, validateFetchTokenDrift, winnerStateDigestOf } from './apply.js';
|
|
3
|
+
export { createChangeMutation, createRemoveMutation, createSetMutation, createTombstoneMutation } from './create.js';
|
|
4
|
+
export { DocumentIDMismatchError, FetchTokenOrderError, HLCDriftError, IncompleteProviderGroupError, InvalidAccessPermissionsError, InvalidFetchTokenError, isPermanentApplyError, MergePolicyRemoveRequiresTombstoneError, MissingFetchTokenError, MutationSchemaValidationError, OwnerChangeError, ProviderGroupChangeRejectedError, TombstoneRequiresMergePolicyError, UnsupportedMutationTypeError, WriteAccessDeniedError } from './errors.js';
|
|
5
5
|
export { convertPatchInput, createMutationOperations } from './operations.js';
|
|
6
|
+
export { DELETED_STATE_DIGEST, stateDigest } from './state-digest.js';
|
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.17.1",
|
|
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.
|
|
18
|
+
"@kubun/hlc": "^0.17.0",
|
|
19
|
+
"@kubun/id": "^0.17.0",
|
|
20
20
|
"@sozai/codec": "^0.4.0",
|
|
21
21
|
"@sozai/patch": "^0.1.3",
|
|
22
22
|
"@sozai/runtime": "^0.1.0",
|
|
23
23
|
"@sozai/schema": "^0.1.2"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@kubun/
|
|
27
|
-
"@kubun/protocol": "^0.16.0"
|
|
26
|
+
"@kubun/protocol": "^0.17.1"
|
|
28
27
|
},
|
|
29
28
|
"publishConfig": {
|
|
30
29
|
"access": "public"
|
|
@@ -32,8 +31,8 @@
|
|
|
32
31
|
"scripts": {
|
|
33
32
|
"build": "pnpm run build:clean && pnpm run build:js && pnpm run build:types",
|
|
34
33
|
"build:clean": "del lib",
|
|
35
|
-
"build:js": "swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
|
|
36
|
-
"build:types": "tsc --emitDeclarationOnly --skipLibCheck",
|
|
34
|
+
"build:js": "del 'lib/**/*.js' && swc src -d ./lib --config-file ../../node_modules/@kigu/dev/swc.json --strip-leading-paths",
|
|
35
|
+
"build:types": "del 'lib/**/*.d.ts' 'lib/**/*.d.ts.map' && tsc --emitDeclarationOnly --skipLibCheck",
|
|
37
36
|
"build:types:ci": "tsc --emitDeclarationOnly --declarationMap false",
|
|
38
37
|
"test": "pnpm run test:types && pnpm run test:unit",
|
|
39
38
|
"test:types": "tsc --noEmit -p tsconfig.test.json",
|