@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.
@@ -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
- export declare const connectorStoreDefinition: StoreDefinition<ConnectorTables, ConnectorStoreAPI>;
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
- export const connectorStoreDefinition = {
4
- name: 'connector',
5
- migrations: getConnectorMigrations,
6
- createAPI: createConnectorStore
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 { connectorStoreDefinition } from './definition.js';
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 { connectorStoreDefinition } from './definition.js';
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).addPrimaryKeyConstraint('kubun_connector_sync_state_pkey', [
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.0",
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.5"
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.0",
24
+ "@kubun/db-postgres": "^0.17.1",
25
25
  "@testcontainers/postgresql": "^12.1.0"
26
26
  },
27
27
  "publishConfig": {