@kubun/store-connector 0.17.0 → 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 +287 -7
- package/lib/api.js +882 -35
- 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 +75 -1
- package/lib/tables.d.ts +57 -0
- package/package.json +3 -3
package/lib/definition.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import type { StoreDefinition } from '@kubun/db';
|
|
2
|
-
import type { ConnectorStoreAPI } from './api.js';
|
|
2
|
+
import type { ConnectorStoreAPI, ConnectorStoreDeps } from './api.js';
|
|
3
3
|
import type { ConnectorTables } from './tables.js';
|
|
4
|
-
|
|
4
|
+
/**
|
|
5
|
+
* The connector store's definition, its API bound to `deps`:
|
|
6
|
+
* `KubunDB.register`/`getStore` call `createAPI(db, adapter)` with exactly two
|
|
7
|
+
* arguments, so the dependencies close over here.
|
|
8
|
+
*/
|
|
9
|
+
export declare function createConnectorStoreDefinition(deps: ConnectorStoreDeps): StoreDefinition<ConnectorTables, ConnectorStoreAPI>;
|
package/lib/definition.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { createConnectorStore } from './api.js';
|
|
2
2
|
import { getConnectorMigrations } from './migrations.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
/**
|
|
4
|
+
* The connector store's definition, its API bound to `deps`:
|
|
5
|
+
* `KubunDB.register`/`getStore` call `createAPI(db, adapter)` with exactly two
|
|
6
|
+
* arguments, so the dependencies close over here.
|
|
7
|
+
*/ export function createConnectorStoreDefinition(deps) {
|
|
8
|
+
return {
|
|
9
|
+
name: 'connector',
|
|
10
|
+
migrations: getConnectorMigrations,
|
|
11
|
+
createAPI: (db, adapter)=>createConnectorStore(db, adapter, deps)
|
|
12
|
+
};
|
|
13
|
+
}
|
package/lib/index.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import type { StoreProvider } from '@kubun/db';
|
|
2
2
|
import type { ConnectorStoreAPI } from './api.js';
|
|
3
|
-
export type { ConnectorStoreAPI, PendingAuthRecord, SyncState, SyncStateEntry, SyncStatus, } from './api.js';
|
|
3
|
+
export type { ArmWatchOptions, ArmWatchOutcome, ArmWatchReason, ClaimOperationInput, ConnectorOperation, ConnectorOperationOp, ConnectorOperationState, ConnectorStoreAPI, ConnectorStoreDeps, DeferReason, FinalizeOperationInput, PendingAuthRecord, ProjectionWatch, ProjectionWatchIdentity, ProjectionWatchVerificationStatus, ReadWinner, RecordOperationInput, RecordVerificationInput, RecordVerificationOutcome, SyncProgressCASCurrent, SyncProgressCASResult, SyncProgressRecompute, SyncState, SyncStateEntry, SyncStatus, TrackedOperationFilter, } from './api.js';
|
|
4
4
|
export { createConnectorStore } from './api.js';
|
|
5
|
-
export {
|
|
5
|
+
export { createConnectorStoreDefinition } from './definition.js';
|
|
6
6
|
export declare const CONNECTOR_STORE: "connector";
|
|
7
7
|
export declare function getConnectorStore(provider: StoreProvider): Promise<ConnectorStoreAPI>;
|
|
8
8
|
export type { ConnectorSyncState, ConnectorSyncStateTable, ConnectorTables, } from './tables.js';
|
package/lib/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { createConnectorStore } from './api.js';
|
|
2
|
-
export {
|
|
2
|
+
export { createConnectorStoreDefinition } from './definition.js';
|
|
3
3
|
export const CONNECTOR_STORE = 'connector';
|
|
4
4
|
export function getConnectorStore(provider) {
|
|
5
5
|
return provider.getStore(CONNECTOR_STORE);
|
package/lib/migrations.js
CHANGED
|
@@ -7,7 +7,13 @@ export function getConnectorMigrations(ctx) {
|
|
|
7
7
|
// overflows int32 on Postgres; bigint holds it and the adapter's int8
|
|
8
8
|
// parser reads it back as a JS number. Nullable: a row with no active
|
|
9
9
|
// lease leaves this null.
|
|
10
|
-
.addColumn('lease_expires_at', t.bigint)
|
|
10
|
+
.addColumn('lease_expires_at', t.bigint)// Durable recovery buffer, JSON-encoded (see @kubun/connector `Recovery`).
|
|
11
|
+
// Nullable: null means an empty buffer.
|
|
12
|
+
.addColumn('recovery', t.text)// Optimistic-concurrency version, bumped on every atomic checkpoint+
|
|
13
|
+
// recovery write. Starts at 0.
|
|
14
|
+
.addColumn('version', 'integer', (col)=>col.notNull().defaultTo(0))// Random id set on insert, part of the CAS predicate so a paused writer
|
|
15
|
+
// cannot rebase stale recovery onto a delete+recreated row (ABA).
|
|
16
|
+
.addColumn('incarnation', t.text).addPrimaryKeyConstraint('kubun_connector_sync_state_pkey', [
|
|
11
17
|
'connector_name',
|
|
12
18
|
'owner_did'
|
|
13
19
|
]).execute();
|
|
@@ -27,8 +33,76 @@ export function getConnectorMigrations(ctx) {
|
|
|
27
33
|
// the adapter's int8 parser reads it back as a JS number. The sweep in
|
|
28
34
|
// `deleteExpiredPendingAuth` keys off this column, not `created_at`.
|
|
29
35
|
.addColumn('expires_at', t.bigint, (col)=>col.notNull()).execute();
|
|
36
|
+
// Connector write-back operation envelope: one row per attempted create/
|
|
37
|
+
// update, keyed by the caller's idempotency key so a replayed request
|
|
38
|
+
// (crash recovery, retry) converges on the same row instead of double-
|
|
39
|
+
// writing to the provider.
|
|
40
|
+
await db.schema.createTable('kubun_connector_operation').ifNotExists().addColumn('caller_did', t.text, (col)=>col.notNull()).addColumn('operation_id', t.text, (col)=>col.notNull())// An opaque random key assigned at claim time, the operation's public
|
|
41
|
+
// identity (its GraphQL node id). Kept apart from the caller-supplied
|
|
42
|
+
// `operation_id`, which is an idempotency key and says nothing about
|
|
43
|
+
// who may look the operation up.
|
|
44
|
+
.addColumn('node_key', t.text, (col)=>col.notNull()).addColumn('owner_did', t.text, (col)=>col.notNull()).addColumn('connector_name', t.text, (col)=>col.notNull()).addColumn('model', t.text, (col)=>col.notNull()).addColumn('op', t.text, (col)=>col.notNull()).addColumn('account_key', t.text, (col)=>col.notNull()).addColumn('target', t.text, (col)=>col.notNull()).addColumn('source_id', t.text).addColumn('deterministic_event_id', t.text, (col)=>col.notNull()).addColumn('payload_hash', t.text, (col)=>col.notNull()).addColumn('reissue_intent', t.text, (col)=>col.notNull()).addColumn('provider_etag', t.text).addColumn('document_id', t.text).addColumn('result_entity', t.text).addColumn('state', t.text, (col)=>col.notNull()).addColumn('attempts', 'integer', (col)=>col.notNull().defaultTo(0))// ms-epoch (Date.now) overflows int32 on Postgres; bigint holds it and
|
|
45
|
+
// the adapter's int8 parser reads it back as a JS number.
|
|
46
|
+
.addColumn('next_attempt_at', t.bigint)// Why `next_attempt_at` was pushed out without spending an attempt, when
|
|
47
|
+
// that reason can lapse on its own: 'offline' is made due again as soon
|
|
48
|
+
// as the device reconnects, 'auth' as soon as the owner authorizes.
|
|
49
|
+
// Null for a backoff or any other deferral.
|
|
50
|
+
.addColumn('defer_reason', t.text)// The run that started a provider write in the current attempt (a
|
|
51
|
+
// per-run marker), cleared when a new attempt begins: a re-run of the
|
|
52
|
+
// same attempt cannot claim no request was ever sent.
|
|
53
|
+
.addColumn('send_marker', t.text).addColumn('last_kind', t.text).addColumn('last_http_status', 'integer').addColumn('version', 'integer', (col)=>col.notNull().defaultTo(0)).addColumn('incarnation', t.text).addColumn('lease_token', t.text)// ms-epoch (Date.now) overflows int32 on Postgres; bigint holds it and
|
|
54
|
+
// the adapter's int8 parser reads it back as a JS number.
|
|
55
|
+
.addColumn('lease_expires_at', t.bigint)// ms-epoch (Date.now) overflows int32 on Postgres; bigint holds it and
|
|
56
|
+
// the adapter's int8 parser reads it back as a JS number.
|
|
57
|
+
.addColumn('created_at', t.bigint, (col)=>col.notNull()).addColumn('updated_at', t.bigint, (col)=>col.notNull()).addPrimaryKeyConstraint('kubun_connector_operation_pkey', [
|
|
58
|
+
'caller_did',
|
|
59
|
+
'operation_id'
|
|
60
|
+
]).execute();
|
|
61
|
+
await db.schema.createIndex('uq_connector_operation_node_key').ifNotExists().on('kubun_connector_operation').column('node_key').unique().execute();
|
|
62
|
+
// The operations of a document (its write state), read by document id.
|
|
63
|
+
await db.schema.createIndex('idx_connector_operation_document').ifNotExists().on('kubun_connector_operation').column('document_id').execute();
|
|
64
|
+
// The operations an owner may see (`listTrackedOperations({ visibleTo })`
|
|
65
|
+
// matches the caller through the primary key, the owner through this).
|
|
66
|
+
await db.schema.createIndex('idx_connector_operation_owner').ifNotExists().on('kubun_connector_operation').column('owner_did').execute();
|
|
67
|
+
// The retention cleanup's candidates: finished operations by last update.
|
|
68
|
+
await db.schema.createIndex('idx_connector_operation_state_updated').ifNotExists().on('kubun_connector_operation').columns([
|
|
69
|
+
'state',
|
|
70
|
+
'updated_at'
|
|
71
|
+
]).execute();
|
|
72
|
+
// Connector projection verify/repair watch: one row per ingested
|
|
73
|
+
// provider document being tracked for eventual verification against the
|
|
74
|
+
// local converged state, with a unique ingress index so the same
|
|
75
|
+
// (owner, model, document) is only ever watched under one source.
|
|
76
|
+
await db.schema.createTable('kubun_connector_projection_watch').ifNotExists().addColumn('owner_did', t.text, (col)=>col.notNull()).addColumn('connector_name', t.text, (col)=>col.notNull()).addColumn('model', t.text, (col)=>col.notNull()).addColumn('account_key', t.text, (col)=>col.notNull()).addColumn('source_id', t.text, (col)=>col.notNull()).addColumn('document_id', t.text, (col)=>col.notNull()).addColumn('verified_state_digest', t.text).addColumn('observed_winner_digest', t.text).addColumn('verification_status', t.text, (col)=>col.notNull()).addColumn('verification_attempts', 'integer', (col)=>col.notNull().defaultTo(0)).addColumn('repair_generation', 'integer', (col)=>col.notNull().defaultTo(0))// ms-epoch (Date.now) overflows int32 on Postgres; bigint holds it and
|
|
77
|
+
// the adapter's int8 parser reads it back as a JS number.
|
|
78
|
+
.addColumn('next_attempt_at', t.bigint)// Until when a verifier holds the current attempt (`claimWatchAttempt`);
|
|
79
|
+
// kept apart from `next_attempt_at`, which an ingress coalesce moves.
|
|
80
|
+
.addColumn('claimed_until', t.bigint)// As on the operation table: 'offline' when the pending attempt was
|
|
81
|
+
// deferred only for being offline.
|
|
82
|
+
.addColumn('defer_reason', t.text).addColumn('version', 'integer', (col)=>col.notNull().defaultTo(0))// ms-epoch (Date.now) overflows int32 on Postgres; bigint holds it and
|
|
83
|
+
// the adapter's int8 parser reads it back as a JS number.
|
|
84
|
+
.addColumn('created_at', t.bigint, (col)=>col.notNull()).addColumn('updated_at', t.bigint, (col)=>col.notNull()).addPrimaryKeyConstraint('kubun_connector_projection_watch_pkey', [
|
|
85
|
+
'owner_did',
|
|
86
|
+
'connector_name',
|
|
87
|
+
'model',
|
|
88
|
+
'account_key',
|
|
89
|
+
'source_id'
|
|
90
|
+
]).execute();
|
|
91
|
+
// Ingress index: within one owner+model, a given provider document may
|
|
92
|
+
// only be watched under a single source. A write-back finalized for the
|
|
93
|
+
// document under another source supersedes the earlier row.
|
|
94
|
+
await db.schema.createIndex('uq_connector_watch_ingress').ifNotExists().on('kubun_connector_projection_watch').columns([
|
|
95
|
+
'owner_did',
|
|
96
|
+
'model',
|
|
97
|
+
'document_id'
|
|
98
|
+
]).unique().execute();
|
|
99
|
+
// Document lookup: an applied write names only its document id
|
|
100
|
+
// (`getWatchByDocument`), which the ingress index cannot serve alone.
|
|
101
|
+
await db.schema.createIndex('idx_connector_watch_document').ifNotExists().on('kubun_connector_projection_watch').column('document_id').execute();
|
|
30
102
|
},
|
|
31
103
|
async down (db) {
|
|
104
|
+
await db.schema.dropTable('kubun_connector_projection_watch').ifExists().execute();
|
|
105
|
+
await db.schema.dropTable('kubun_connector_operation').ifExists().execute();
|
|
32
106
|
await db.schema.dropTable('kubun_connector_pending_auth').ifExists().execute();
|
|
33
107
|
await db.schema.dropTable('kubun_connector_sync_state').ifExists().execute();
|
|
34
108
|
}
|
package/lib/tables.d.ts
CHANGED
|
@@ -8,6 +8,9 @@ export type ConnectorSyncStateTable = {
|
|
|
8
8
|
status: string;
|
|
9
9
|
error: string | null;
|
|
10
10
|
lease_expires_at: number | null;
|
|
11
|
+
recovery: string | null;
|
|
12
|
+
version: number;
|
|
13
|
+
incarnation: string | null;
|
|
11
14
|
};
|
|
12
15
|
export type ConnectorSyncState = Selectable<ConnectorSyncStateTable>;
|
|
13
16
|
export type ConnectorPendingAuthTable = {
|
|
@@ -34,7 +37,61 @@ export type ConnectorPendingAuthTable = {
|
|
|
34
37
|
expires_at: number;
|
|
35
38
|
};
|
|
36
39
|
export type ConnectorPendingAuth = Selectable<ConnectorPendingAuthTable>;
|
|
40
|
+
export type ConnectorOperationTable = {
|
|
41
|
+
caller_did: string;
|
|
42
|
+
operation_id: string;
|
|
43
|
+
node_key: string;
|
|
44
|
+
owner_did: string;
|
|
45
|
+
connector_name: string;
|
|
46
|
+
model: string;
|
|
47
|
+
op: string;
|
|
48
|
+
account_key: string;
|
|
49
|
+
target: string;
|
|
50
|
+
source_id: string | null;
|
|
51
|
+
deterministic_event_id: string;
|
|
52
|
+
payload_hash: string;
|
|
53
|
+
reissue_intent: string;
|
|
54
|
+
provider_etag: string | null;
|
|
55
|
+
document_id: string | null;
|
|
56
|
+
result_entity: string | null;
|
|
57
|
+
state: string;
|
|
58
|
+
attempts: number;
|
|
59
|
+
next_attempt_at: number | null;
|
|
60
|
+
last_kind: string | null;
|
|
61
|
+
last_http_status: number | null;
|
|
62
|
+
defer_reason: string | null;
|
|
63
|
+
send_marker: string | null;
|
|
64
|
+
version: number;
|
|
65
|
+
incarnation: string | null;
|
|
66
|
+
lease_token: string | null;
|
|
67
|
+
lease_expires_at: number | null;
|
|
68
|
+
created_at: number;
|
|
69
|
+
updated_at: number;
|
|
70
|
+
};
|
|
71
|
+
export type ConnectorOperation = Selectable<ConnectorOperationTable>;
|
|
72
|
+
export type ConnectorProjectionWatchTable = {
|
|
73
|
+
owner_did: string;
|
|
74
|
+
connector_name: string;
|
|
75
|
+
model: string;
|
|
76
|
+
account_key: string;
|
|
77
|
+
source_id: string;
|
|
78
|
+
document_id: string;
|
|
79
|
+
verified_state_digest: string | null;
|
|
80
|
+
observed_winner_digest: string | null;
|
|
81
|
+
verification_status: string;
|
|
82
|
+
verification_attempts: number;
|
|
83
|
+
repair_generation: number;
|
|
84
|
+
next_attempt_at: number | null;
|
|
85
|
+
claimed_until: number | null;
|
|
86
|
+
defer_reason: string | null;
|
|
87
|
+
version: number;
|
|
88
|
+
created_at: number;
|
|
89
|
+
updated_at: number;
|
|
90
|
+
};
|
|
91
|
+
export type ConnectorProjectionWatch = Selectable<ConnectorProjectionWatchTable>;
|
|
37
92
|
export type ConnectorTables = {
|
|
38
93
|
kubun_connector_sync_state: ConnectorSyncStateTable;
|
|
39
94
|
kubun_connector_pending_auth: ConnectorPendingAuthTable;
|
|
95
|
+
kubun_connector_operation: ConnectorOperationTable;
|
|
96
|
+
kubun_connector_projection_watch: ConnectorProjectionWatchTable;
|
|
40
97
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kubun/store-connector",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.2",
|
|
4
4
|
"keywords": [],
|
|
5
5
|
"license": "see LICENSE.md",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@kubun/db": "^0.17.0",
|
|
19
19
|
"@kubun/db-adapter": "^0.17.0",
|
|
20
|
-
"kysely": "^0.29.
|
|
20
|
+
"kysely": "^0.29.6"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"@kubun/db-better-sqlite": "^0.17.0",
|
|
24
|
-
"@kubun/db-postgres": "^0.17.
|
|
24
|
+
"@kubun/db-postgres": "^0.17.1",
|
|
25
25
|
"@testcontainers/postgresql": "^12.1.0"
|
|
26
26
|
},
|
|
27
27
|
"publishConfig": {
|