@kubun/store-connector 0.17.1 → 0.17.2
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/api.d.ts +268 -2
- package/lib/api.js +803 -4
- package/lib/definition.d.ts +7 -2
- package/lib/definition.js +11 -5
- package/lib/index.d.ts +2 -2
- package/lib/index.js +1 -1
- package/lib/migrations.js +68 -0
- package/lib/tables.d.ts +54 -0
- package/package.json +3 -3
package/lib/api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Adapter } from '@kubun/db-adapter';
|
|
2
2
|
import type { Kysely } from 'kysely';
|
|
3
3
|
import type { ConnectorTables } from './tables.js';
|
|
4
4
|
export type SyncStatus = 'idle' | 'syncing' | 'error';
|
|
@@ -47,6 +47,207 @@ export type PendingAuthRecord = {
|
|
|
47
47
|
scopes: Array<string>;
|
|
48
48
|
expiresAt: number;
|
|
49
49
|
};
|
|
50
|
+
export type ConnectorOperationOp = 'create' | 'update';
|
|
51
|
+
export type ConnectorOperationState = 'attempted' | 'committed' | 'reconcile_only' | 'dead';
|
|
52
|
+
export type ClaimOperationInput = {
|
|
53
|
+
callerDID: string;
|
|
54
|
+
operationID: string;
|
|
55
|
+
ownerDID: string;
|
|
56
|
+
connectorName: string;
|
|
57
|
+
model: string;
|
|
58
|
+
op: ConnectorOperationOp;
|
|
59
|
+
accountKey: string;
|
|
60
|
+
target: string;
|
|
61
|
+
sourceID: string | null;
|
|
62
|
+
deterministicEventID: string;
|
|
63
|
+
payloadHash: string;
|
|
64
|
+
reissueIntent: unknown;
|
|
65
|
+
/**
|
|
66
|
+
* The provider's conditional-write token (e.g. an ETag) of the state the
|
|
67
|
+
* write is based on, pinned for every attempt of the operation. Captured
|
|
68
|
+
* state rather than part of the envelope: a replay never compares it.
|
|
69
|
+
*/
|
|
70
|
+
providerEtag?: string | null;
|
|
71
|
+
/**
|
|
72
|
+
* The document the operation writes, when it is known before any provider
|
|
73
|
+
* I/O (an update's target, a create whose provider id is predetermined).
|
|
74
|
+
* Recorded on the first claim only; like `providerEtag`, a replay never
|
|
75
|
+
* compares it. Finalize records the document the write actually projected
|
|
76
|
+
* into, which takes precedence.
|
|
77
|
+
*/
|
|
78
|
+
documentID?: string | null;
|
|
79
|
+
leaseToken: string;
|
|
80
|
+
leaseExpiresAt: number;
|
|
81
|
+
};
|
|
82
|
+
export type RecordOperationInput = {
|
|
83
|
+
callerDID: string;
|
|
84
|
+
operationID: string;
|
|
85
|
+
fromState: ConnectorOperationState;
|
|
86
|
+
toState: Exclude<ConnectorOperationState, 'committed'>;
|
|
87
|
+
version: number;
|
|
88
|
+
leaseToken: string;
|
|
89
|
+
providerEtag?: string | null;
|
|
90
|
+
nextAttemptAt?: number | null;
|
|
91
|
+
kind?: string | null;
|
|
92
|
+
httpStatus?: number | null;
|
|
93
|
+
/**
|
|
94
|
+
* Whether this transition consumes a recovery attempt (default `true`).
|
|
95
|
+
* `false` records a failure's classification against an attempt already
|
|
96
|
+
* consumed earlier in the same recovery pass.
|
|
97
|
+
*/
|
|
98
|
+
countAttempt?: boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Gives back the attempt this recovery pass consumed (`attempts - 1`), for a
|
|
101
|
+
* pass that turned out never to reach the provider. Takes precedence over
|
|
102
|
+
* `countAttempt`.
|
|
103
|
+
*/
|
|
104
|
+
refundAttempt?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Marks the `nextAttemptAt` this records as a deferral that lapses on its
|
|
107
|
+
* own (see `DeferReason`). Omitted clears any earlier mark.
|
|
108
|
+
*/
|
|
109
|
+
deferReason?: DeferReason;
|
|
110
|
+
/**
|
|
111
|
+
* With `refundAttempt`: the marker of the run asking (see
|
|
112
|
+
* `markSendStarted`). The refund is refused when a different run started
|
|
113
|
+
* a provider write in this attempt, including when this run marked too.
|
|
114
|
+
*/
|
|
115
|
+
sendMarker?: string;
|
|
116
|
+
};
|
|
117
|
+
/**
|
|
118
|
+
* A deferral that lapses on its own: `'offline'` — the device was offline,
|
|
119
|
+
* made due again on reconnect (`resumeOfflineDeferred`); `'auth'` — the
|
|
120
|
+
* owner's credential could not authorize the write (none stored, or refused by
|
|
121
|
+
* the provider), made due again once the owner authorizes
|
|
122
|
+
* (`resumeAuthDeferred`).
|
|
123
|
+
*/
|
|
124
|
+
export type DeferReason = 'offline' | 'auth';
|
|
125
|
+
export type FinalizeOperationInput = {
|
|
126
|
+
key: {
|
|
127
|
+
callerDID: string;
|
|
128
|
+
operationID: string;
|
|
129
|
+
};
|
|
130
|
+
version: number;
|
|
131
|
+
leaseToken: string;
|
|
132
|
+
documentID: string;
|
|
133
|
+
resultEntity: unknown;
|
|
134
|
+
watchIdentity: ProjectionWatchIdentity;
|
|
135
|
+
currentWinnerDigest: string | null;
|
|
136
|
+
};
|
|
137
|
+
export type ConnectorOperation = {
|
|
138
|
+
callerDID: string;
|
|
139
|
+
operationID: string;
|
|
140
|
+
/**
|
|
141
|
+
* Opaque random key assigned when the operation was first claimed: its
|
|
142
|
+
* public identity, unlike `operationID`, which is the caller's idempotency
|
|
143
|
+
* key.
|
|
144
|
+
*/
|
|
145
|
+
nodeKey: string;
|
|
146
|
+
ownerDID: string;
|
|
147
|
+
connectorName: string;
|
|
148
|
+
model: string;
|
|
149
|
+
op: ConnectorOperationOp;
|
|
150
|
+
accountKey: string;
|
|
151
|
+
target: string;
|
|
152
|
+
sourceID: string | null;
|
|
153
|
+
deterministicEventID: string;
|
|
154
|
+
payloadHash: string;
|
|
155
|
+
reissueIntent: unknown;
|
|
156
|
+
providerEtag: string | null;
|
|
157
|
+
documentID: string | null;
|
|
158
|
+
resultEntity: unknown;
|
|
159
|
+
state: ConnectorOperationState;
|
|
160
|
+
attempts: number;
|
|
161
|
+
nextAttemptAt: number | null;
|
|
162
|
+
lastKind: string | null;
|
|
163
|
+
lastHTTPStatus: number | null;
|
|
164
|
+
version: number;
|
|
165
|
+
incarnation: string | null;
|
|
166
|
+
leaseToken: string | null;
|
|
167
|
+
leaseExpiresAt: number | null;
|
|
168
|
+
createdAt: number;
|
|
169
|
+
updatedAt: number;
|
|
170
|
+
};
|
|
171
|
+
export type ProjectionWatchIdentity = {
|
|
172
|
+
ownerDID: string;
|
|
173
|
+
connectorName: string;
|
|
174
|
+
model: string;
|
|
175
|
+
accountKey: string;
|
|
176
|
+
sourceID: string;
|
|
177
|
+
};
|
|
178
|
+
export type ProjectionWatchVerificationStatus = 'pending_verification' | 'verified' | 'repair_failed';
|
|
179
|
+
export type ProjectionWatch = {
|
|
180
|
+
ownerDID: string;
|
|
181
|
+
connectorName: string;
|
|
182
|
+
model: string;
|
|
183
|
+
accountKey: string;
|
|
184
|
+
sourceID: string;
|
|
185
|
+
documentID: string;
|
|
186
|
+
verifiedStateDigest: string | null;
|
|
187
|
+
observedWinnerDigest: string | null;
|
|
188
|
+
verificationStatus: ProjectionWatchVerificationStatus;
|
|
189
|
+
verificationAttempts: number;
|
|
190
|
+
repairGeneration: number;
|
|
191
|
+
nextAttemptAt: number | null;
|
|
192
|
+
version: number;
|
|
193
|
+
createdAt: number;
|
|
194
|
+
updatedAt: number;
|
|
195
|
+
};
|
|
196
|
+
/**
|
|
197
|
+
* Filters for `listTrackedOperations`, all optional and combined with AND. An
|
|
198
|
+
* empty `states` or `documentIDs` list matches nothing.
|
|
199
|
+
*/
|
|
200
|
+
export type TrackedOperationFilter = {
|
|
201
|
+
nodeKey?: string;
|
|
202
|
+
callerDID?: string;
|
|
203
|
+
operationID?: string;
|
|
204
|
+
/** Operations the DID claimed as caller, or owns as credential owner. */
|
|
205
|
+
visibleTo?: string;
|
|
206
|
+
connectorName?: string;
|
|
207
|
+
states?: Array<ConnectorOperationState>;
|
|
208
|
+
/** Operations linked to one of these documents. */
|
|
209
|
+
documentIDs?: Array<string>;
|
|
210
|
+
};
|
|
211
|
+
export type ArmWatchReason = 'finalize' | 'ingress' | 'manual';
|
|
212
|
+
export type ArmWatchOutcome = 'opened' | 'coalesced' | 'noop';
|
|
213
|
+
export type ArmWatchOptions = {
|
|
214
|
+
triggeringWinner?: string;
|
|
215
|
+
dueAt: number;
|
|
216
|
+
};
|
|
217
|
+
export type RecordVerificationInput = {
|
|
218
|
+
fetchedDigest: string | null;
|
|
219
|
+
generation: number;
|
|
220
|
+
maxAttempts: number;
|
|
221
|
+
dueAt: number;
|
|
222
|
+
claimedUntil?: number;
|
|
223
|
+
};
|
|
224
|
+
export type RecordVerificationOutcome = 'verified' | 'pending' | 'repair_failed' | 'stale-generation' | 'stale-claim';
|
|
225
|
+
/**
|
|
226
|
+
* Reads a document's current winner state digest (`W`) INSIDE the caller's
|
|
227
|
+
* transaction — wraps `readWinnerStateDigest(tx, documentID)`
|
|
228
|
+
* (exported from `@kubun/store-graph`). Graph and connector tables share one
|
|
229
|
+
* DB/connection via the KubunDB coordinator, so the wired implementation
|
|
230
|
+
* must be invoked with the `tx` handed to this callback, never a top-level
|
|
231
|
+
* `db` — passing the top-level db would nest a transaction and, on SQLite's
|
|
232
|
+
* single connection, deadlock.
|
|
233
|
+
*/
|
|
234
|
+
export type ReadWinner = (documentID: string, tx: Kysely<ConnectorTables>) => Promise<string>;
|
|
235
|
+
export type ConnectorStoreDeps = {
|
|
236
|
+
/**
|
|
237
|
+
* Required by `armWatch`'s `'ingress'`/`'manual'` reasons and by
|
|
238
|
+
* `recordVerification`; a caller that reaches either without it gets a clear
|
|
239
|
+
* throw rather than a silent no-op. The connector plugin passes one over
|
|
240
|
+
* `readWinnerStateDigest`; tests pass a scripted fake so the CAS interleaves
|
|
241
|
+
* are deterministic.
|
|
242
|
+
*/
|
|
243
|
+
readWinner?: ReadWinner;
|
|
244
|
+
/**
|
|
245
|
+
* The source of every random id the store mints: an operation's node key and
|
|
246
|
+
* a sync state's incarnation. The connector plugin passes its runtime's
|
|
247
|
+
* `getRandomID`.
|
|
248
|
+
*/
|
|
249
|
+
getRandomID: () => string;
|
|
250
|
+
};
|
|
50
251
|
export type ConnectorStoreAPI = {
|
|
51
252
|
getSyncState(connectorName: string, ownerDID: string): Promise<SyncState | null>;
|
|
52
253
|
setSyncState(connectorName: string, ownerDID: string, state: SyncState): Promise<void>;
|
|
@@ -58,5 +259,70 @@ export type ConnectorStoreAPI = {
|
|
|
58
259
|
createPendingAuth(record: PendingAuthRecord): Promise<void>;
|
|
59
260
|
consumePendingAuth(state: string): Promise<PendingAuthRecord | null>;
|
|
60
261
|
deleteExpiredPendingAuth(cutoffMs: number): Promise<void>;
|
|
262
|
+
claimOperation(input: ClaimOperationInput): Promise<{
|
|
263
|
+
status: 'claimed';
|
|
264
|
+
record: ConnectorOperation;
|
|
265
|
+
} | {
|
|
266
|
+
status: 'exists';
|
|
267
|
+
record: ConnectorOperation;
|
|
268
|
+
} | {
|
|
269
|
+
status: 'conflict';
|
|
270
|
+
}>;
|
|
271
|
+
acquireLease(callerDID: string, operationID: string, token: string, expiresAt: number): Promise<boolean>;
|
|
272
|
+
renewLease(callerDID: string, operationID: string, token: string, expiresAt: number): Promise<boolean>;
|
|
273
|
+
releaseLease(callerDID: string, operationID: string, token: string): Promise<boolean>;
|
|
274
|
+
getOperation(callerDID: string, operationID: string): Promise<ConnectorOperation | null>;
|
|
275
|
+
listTrackedOperations(filter: TrackedOperationFilter): Promise<Array<ConnectorOperation>>;
|
|
276
|
+
hasNewerOperationOnDocument(callerDID: string, operationID: string): Promise<boolean>;
|
|
277
|
+
markSendStarted(params: {
|
|
278
|
+
callerDID: string;
|
|
279
|
+
operationID: string;
|
|
280
|
+
leaseToken: string;
|
|
281
|
+
marker: string;
|
|
282
|
+
}): Promise<boolean>;
|
|
283
|
+
deferOperation(params: {
|
|
284
|
+
callerDID: string;
|
|
285
|
+
operationID: string;
|
|
286
|
+
nextAttemptAt: number;
|
|
287
|
+
leaseToken?: string;
|
|
288
|
+
reason?: DeferReason;
|
|
289
|
+
}): Promise<boolean>;
|
|
290
|
+
recordOperation(input: RecordOperationInput): Promise<boolean>;
|
|
291
|
+
finalizeOperationAndArmWatch(input: FinalizeOperationInput): Promise<boolean>;
|
|
292
|
+
getWatch(identity: ProjectionWatchIdentity): Promise<ProjectionWatch | null>;
|
|
293
|
+
getWatchByDocument(documentID: string): Promise<ProjectionWatch | null>;
|
|
294
|
+
listWatchesByDocuments(documentIDs: Array<string>): Promise<Array<ProjectionWatch>>;
|
|
295
|
+
armWatch(identity: ProjectionWatchIdentity, reason: ArmWatchReason, opts: ArmWatchOptions): Promise<ArmWatchOutcome>;
|
|
296
|
+
deferWatch(identity: ProjectionWatchIdentity, opts: {
|
|
297
|
+
generation: number;
|
|
298
|
+
nextAttemptAt: number;
|
|
299
|
+
reason?: DeferReason;
|
|
300
|
+
claimedUntil: number;
|
|
301
|
+
}): Promise<boolean>;
|
|
302
|
+
resumeOfflineDeferred(now: number): Promise<void>;
|
|
303
|
+
resumeAuthDeferred(params: {
|
|
304
|
+
now: number;
|
|
305
|
+
ownerDID: string;
|
|
306
|
+
connectorNames: Array<string>;
|
|
307
|
+
}): Promise<void>;
|
|
308
|
+
claimWatchAttempt(identity: ProjectionWatchIdentity, opts: {
|
|
309
|
+
generation: number;
|
|
310
|
+
now: number;
|
|
311
|
+
until: number;
|
|
312
|
+
}): Promise<boolean>;
|
|
313
|
+
recordVerification(identity: ProjectionWatchIdentity, input: RecordVerificationInput): Promise<RecordVerificationOutcome>;
|
|
314
|
+
requestRepull(identity: ProjectionWatchIdentity, opts: {
|
|
315
|
+
dueAt: number;
|
|
316
|
+
}): Promise<void>;
|
|
317
|
+
listDueOperations(now: number, opts: {
|
|
318
|
+
readWinner: ReadWinner;
|
|
319
|
+
}): Promise<{
|
|
320
|
+
operations: Array<ConnectorOperation>;
|
|
321
|
+
watches: Array<ProjectionWatch>;
|
|
322
|
+
}>;
|
|
323
|
+
collectFinishedOperations(params: {
|
|
324
|
+
before: number;
|
|
325
|
+
limit: number;
|
|
326
|
+
}): Promise<number>;
|
|
61
327
|
};
|
|
62
|
-
export declare function createConnectorStore(db: Kysely<ConnectorTables>,
|
|
328
|
+
export declare function createConnectorStore(db: Kysely<ConnectorTables>, adapter: Adapter, deps: ConnectorStoreDeps): ConnectorStoreAPI;
|