@syncular/server 0.15.44 → 0.15.46
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/README.md +135 -5
- package/dist/admin.d.ts +12 -6
- package/dist/admin.js +11 -1
- package/dist/authoritative-query.d.ts +20 -0
- package/dist/authoritative-query.js +184 -0
- package/dist/blob-store.d.ts +2 -2
- package/dist/context.d.ts +11 -2
- package/dist/context.js +2 -0
- package/dist/d1-storage.d.ts +10 -1
- package/dist/d1-storage.js +219 -3
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +43 -1
- package/dist/events-ring.d.ts +1 -1
- package/dist/events-ring.js +1 -1
- package/dist/events.d.ts +52 -3
- package/dist/handler.js +6 -3
- package/dist/index.d.ts +5 -1
- package/dist/index.js +5 -1
- package/dist/operations-realtime.d.ts +16 -0
- package/dist/operations-realtime.js +196 -0
- package/dist/operations.d.ts +97 -0
- package/dist/operations.js +392 -0
- package/dist/pg-executor.d.ts +1 -1
- package/dist/pg-executor.js +1 -1
- package/dist/postgres-fanout.d.ts +1 -1
- package/dist/postgres-storage.d.ts +11 -2
- package/dist/postgres-storage.js +223 -3
- package/dist/pull.js +1 -1
- package/dist/push.d.ts +8 -2
- package/dist/push.js +75 -21
- package/dist/reactions.d.ts +167 -0
- package/dist/reactions.js +442 -0
- package/dist/realtime.js +4 -1
- package/dist/relational-rows.d.ts +9 -9
- package/dist/relational-rows.js +9 -9
- package/dist/s3-blob-store.js +1 -1
- package/dist/s3-segment-store.js +1 -1
- package/dist/schema.d.ts +4 -5
- package/dist/schema.js +3 -3
- package/dist/seed.js +1 -1
- package/dist/segment-store.d.ts +3 -3
- package/dist/signed-url.js +2 -2
- package/dist/sigv4.d.ts +2 -4
- package/dist/sigv4.js +3 -3
- package/dist/sqlite-blob-store.d.ts +1 -1
- package/dist/sqlite-blob-store.js +1 -1
- package/dist/sqlite-dialect.d.ts +3 -4
- package/dist/sqlite-dialect.js +21 -1
- package/dist/sqlite-image.d.ts +1 -1
- package/dist/sqlite-lease-store.d.ts +1 -1
- package/dist/sqlite-lease-store.js +1 -1
- package/dist/sqlite-segment-store.d.ts +2 -2
- package/dist/sqlite-segment-store.js +2 -2
- package/dist/sqlite-storage.d.ts +11 -2
- package/dist/sqlite-storage.js +219 -4
- package/dist/storage.d.ts +112 -3
- package/dist/validate.js +1 -0
- package/package.json +2 -2
- package/src/admin.ts +30 -6
- package/src/authoritative-query.ts +218 -0
- package/src/blob-store.ts +0 -0
- package/src/context.ts +12 -2
- package/src/d1-storage.ts +355 -3
- package/src/errors.ts +43 -1
- package/src/events-ring.ts +1 -1
- package/src/events.ts +64 -2
- package/src/handler.ts +15 -3
- package/src/index.ts +33 -1
- package/src/operations-realtime.ts +272 -0
- package/src/operations.ts +720 -0
- package/src/pg-executor.ts +1 -1
- package/src/postgres-fanout.ts +1 -1
- package/src/postgres-storage.ts +355 -4
- package/src/pull.ts +1 -1
- package/src/push.ts +97 -29
- package/src/reactions.ts +741 -0
- package/src/realtime.ts +7 -1
- package/src/relational-rows.ts +9 -9
- package/src/s3-blob-store.ts +1 -1
- package/src/s3-segment-store.ts +1 -1
- package/src/schema.ts +6 -7
- package/src/seed.ts +1 -1
- package/src/segment-store.ts +3 -3
- package/src/signed-url.ts +2 -2
- package/src/sigv4.ts +3 -5
- package/src/sqlite-blob-store.ts +1 -1
- package/src/sqlite-dialect.ts +22 -3
- package/src/sqlite-image.ts +1 -1
- package/src/sqlite-lease-store.ts +1 -1
- package/src/sqlite-segment-store.ts +2 -2
- package/src/sqlite-storage.ts +369 -4
- package/src/storage.ts +168 -3
- package/src/validate.ts +1 -0
package/dist/pg-executor.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The minimal Postgres executor seam
|
|
2
|
+
* The minimal Postgres executor seam has zero runtime dependencies.
|
|
3
3
|
*
|
|
4
4
|
* `PostgresServerStorage` is written entirely against this interface so the
|
|
5
5
|
* server library never imports a specific driver. Production wires one of:
|
package/dist/pg-executor.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The minimal Postgres executor seam
|
|
2
|
+
* The minimal Postgres executor seam has zero runtime dependencies.
|
|
3
3
|
*
|
|
4
4
|
* `PostgresServerStorage` is written entirely against this interface so the
|
|
5
5
|
* server library never imports a specific driver. Production wires one of:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type PgExecutor } from './pg-executor.js';
|
|
2
2
|
import type { CompiledSchema, CompiledTable } from './schema.js';
|
|
3
|
-
import type { ClientCursorInfo, ClientRecord, CommitMetadata, CommitMetadataQuery, CommitWindowQuery, IndexRowScanQuery, RowScanQuery, ScopeActivityQuery, ScopeCommitActivity, ServerStorage, StorageTransaction, StoredCommit, StoredPushResult, StoredRow } from './storage.js';
|
|
3
|
+
import type { AuthoritativeQueryRequest, AuthoritativeQueryResult, ClientCursorInfo, ClientRecord, CommitMetadata, CommitMetadataQuery, CommitWindowQuery, IndexRowScanQuery, PrunedReactionCounts, ReactionClaimQuery, ReactionFailureUpdate, ReactionListQuery, ReactionPruneQuery, RowScanQuery, ScopeActivityQuery, ScopeCommitActivity, ServerStorage, StorageTransaction, StoredCommit, StoredPushResult, StoredReaction, StoredRow } from './storage.js';
|
|
4
4
|
/**
|
|
5
5
|
* Schema DDL. Applied by `PostgresServerStorage.migrate()` (idempotent).
|
|
6
6
|
*
|
|
@@ -23,7 +23,7 @@ import type { ClientCursorInfo, ClientRecord, CommitMetadata, CommitMetadataQuer
|
|
|
23
23
|
* set, §5.9.5) as an index range, never a scan. `postgres-explain
|
|
24
24
|
* .test.ts` asserts an `Index` node here too.
|
|
25
25
|
*/
|
|
26
|
-
export declare const POSTGRES_DDL = "\nCREATE TABLE IF NOT EXISTS sync_partitions(\n partition TEXT PRIMARY KEY,\n max_commit_seq BIGINT NOT NULL DEFAULT 0,\n horizon_seq BIGINT NOT NULL DEFAULT 0\n);\nCREATE TABLE IF NOT EXISTS sync_row_scopes(\n partition TEXT NOT NULL, tbl TEXT NOT NULL,\n var TEXT NOT NULL, value TEXT NOT NULL, row_id TEXT NOT NULL,\n PRIMARY KEY(partition, tbl, var, value, row_id)\n);\nCREATE TABLE IF NOT EXISTS sync_commits(\n partition TEXT NOT NULL, commit_seq BIGINT NOT NULL,\n client_id TEXT NOT NULL, client_commit_id TEXT NOT NULL,\n actor_id TEXT NOT NULL, created_at_ms BIGINT NOT NULL,\n PRIMARY KEY(partition, commit_seq)\n);\nCREATE INDEX IF NOT EXISTS sync_commits_by_time\n ON sync_commits(partition, created_at_ms);\nCREATE TABLE IF NOT EXISTS sync_changes(\n partition TEXT NOT NULL, commit_seq BIGINT NOT NULL, idx INTEGER NOT NULL,\n tbl TEXT NOT NULL, row_id TEXT NOT NULL, op SMALLINT NOT NULL,\n row_version BIGINT, scopes JSONB NOT NULL, payload BYTEA,\n PRIMARY KEY(partition, commit_seq, idx)\n);\nCREATE INDEX IF NOT EXISTS sync_changes_by_table\n ON sync_changes(partition, commit_seq, tbl, idx);\nCREATE TABLE IF NOT EXISTS sync_change_scopes(\n partition TEXT NOT NULL, tbl TEXT NOT NULL,\n var TEXT NOT NULL, value TEXT NOT NULL, commit_seq BIGINT NOT NULL,\n PRIMARY KEY(partition, tbl, var, value, commit_seq)\n);\nCREATE TABLE IF NOT EXISTS sync_push_results(\n partition TEXT NOT NULL, client_id TEXT NOT NULL,\n client_commit_id TEXT NOT NULL, result JSONB NOT NULL,\n PRIMARY KEY(partition, client_id, client_commit_id)\n);\nCREATE TABLE IF NOT EXISTS sync_clients(\n partition TEXT NOT NULL, client_id TEXT NOT NULL, actor_id TEXT NOT NULL,\n cursor BIGINT NOT NULL, subscriptions JSONB NOT NULL,\n updated_at_ms BIGINT NOT NULL,\n PRIMARY KEY(partition, client_id)\n);\nCREATE TABLE IF NOT EXISTS sync_blob_refs(\n partition TEXT NOT NULL, tbl TEXT NOT NULL, row_id TEXT NOT NULL,\n blob_id TEXT NOT NULL,\n PRIMARY KEY(partition, tbl, row_id, blob_id)\n);\nCREATE INDEX IF NOT EXISTS sync_blob_refs_by_blob\n ON sync_blob_refs(partition, blob_id);\n";
|
|
26
|
+
export declare const POSTGRES_DDL = "\nCREATE TABLE IF NOT EXISTS sync_partitions(\n partition TEXT PRIMARY KEY,\n max_commit_seq BIGINT NOT NULL DEFAULT 0,\n horizon_seq BIGINT NOT NULL DEFAULT 0\n);\nCREATE TABLE IF NOT EXISTS sync_row_scopes(\n partition TEXT NOT NULL, tbl TEXT NOT NULL,\n var TEXT NOT NULL, value TEXT NOT NULL, row_id TEXT NOT NULL,\n PRIMARY KEY(partition, tbl, var, value, row_id)\n);\nCREATE TABLE IF NOT EXISTS sync_commits(\n partition TEXT NOT NULL, commit_seq BIGINT NOT NULL,\n client_id TEXT NOT NULL, client_commit_id TEXT NOT NULL,\n actor_id TEXT NOT NULL, created_at_ms BIGINT NOT NULL,\n PRIMARY KEY(partition, commit_seq)\n);\nCREATE INDEX IF NOT EXISTS sync_commits_by_time\n ON sync_commits(partition, created_at_ms);\nCREATE TABLE IF NOT EXISTS sync_changes(\n partition TEXT NOT NULL, commit_seq BIGINT NOT NULL, idx INTEGER NOT NULL,\n tbl TEXT NOT NULL, row_id TEXT NOT NULL, op SMALLINT NOT NULL,\n row_version BIGINT, scopes JSONB NOT NULL, payload BYTEA,\n PRIMARY KEY(partition, commit_seq, idx)\n);\nCREATE INDEX IF NOT EXISTS sync_changes_by_table\n ON sync_changes(partition, commit_seq, tbl, idx);\nCREATE TABLE IF NOT EXISTS sync_change_scopes(\n partition TEXT NOT NULL, tbl TEXT NOT NULL,\n var TEXT NOT NULL, value TEXT NOT NULL, commit_seq BIGINT NOT NULL,\n PRIMARY KEY(partition, tbl, var, value, commit_seq)\n);\nCREATE TABLE IF NOT EXISTS sync_push_results(\n partition TEXT NOT NULL, client_id TEXT NOT NULL,\n client_commit_id TEXT NOT NULL, result JSONB NOT NULL,\n PRIMARY KEY(partition, client_id, client_commit_id)\n);\nCREATE TABLE IF NOT EXISTS sync_reactions(\n partition TEXT NOT NULL, idempotency_key TEXT NOT NULL,\n type TEXT NOT NULL, version INTEGER NOT NULL, payload JSONB NOT NULL,\n source_client_id TEXT NOT NULL, source_client_commit_id TEXT NOT NULL,\n source_commit_seq BIGINT NOT NULL, created_at_ms BIGINT NOT NULL,\n available_at_ms BIGINT NOT NULL, status TEXT NOT NULL,\n attempts INTEGER NOT NULL, max_attempts INTEGER NOT NULL,\n lease_owner TEXT, lease_expires_at_ms BIGINT, completed_at_ms BIGINT,\n last_failure JSONB,\n PRIMARY KEY(partition, idempotency_key),\n CHECK(status IN ('pending', 'leased', 'completed', 'dead-letter'))\n);\nCREATE INDEX IF NOT EXISTS sync_reactions_due\n ON sync_reactions(partition, status, available_at_ms, created_at_ms, idempotency_key);\nCREATE INDEX IF NOT EXISTS sync_reactions_lease\n ON sync_reactions(partition, status, lease_expires_at_ms);\nCREATE INDEX IF NOT EXISTS sync_reactions_completed\n ON sync_reactions(partition, status, completed_at_ms, idempotency_key);\nCREATE INDEX IF NOT EXISTS sync_reactions_dead_letter\n ON sync_reactions(partition, status, available_at_ms, idempotency_key);\nCREATE TABLE IF NOT EXISTS sync_clients(\n partition TEXT NOT NULL, client_id TEXT NOT NULL, actor_id TEXT NOT NULL,\n cursor BIGINT NOT NULL, subscriptions JSONB NOT NULL,\n updated_at_ms BIGINT NOT NULL,\n PRIMARY KEY(partition, client_id)\n);\nCREATE TABLE IF NOT EXISTS sync_blob_refs(\n partition TEXT NOT NULL, tbl TEXT NOT NULL, row_id TEXT NOT NULL,\n blob_id TEXT NOT NULL,\n PRIMARY KEY(partition, tbl, row_id, blob_id)\n);\nCREATE INDEX IF NOT EXISTS sync_blob_refs_by_blob\n ON sync_blob_refs(partition, blob_id);\n";
|
|
27
27
|
export declare class PostgresServerStorage implements ServerStorage {
|
|
28
28
|
#private;
|
|
29
29
|
constructor(exec: PgExecutor);
|
|
@@ -42,12 +42,21 @@ export declare class PostgresServerStorage implements ServerStorage {
|
|
|
42
42
|
*/
|
|
43
43
|
begin(partition: string): Promise<StorageTransaction>;
|
|
44
44
|
getMaxCommitSeq(partition: string): Promise<number>;
|
|
45
|
+
queryAuthoritative(partition: string, query: AuthoritativeQueryRequest): Promise<AuthoritativeQueryResult>;
|
|
45
46
|
getHorizonSeq(partition: string): Promise<number>;
|
|
46
47
|
setHorizonSeq(partition: string, seq: number): Promise<void>;
|
|
47
48
|
pruneCommitsThrough(partition: string, seq: number): Promise<number>;
|
|
48
49
|
getCommitSeqBefore(partition: string, createdBeforeMs: number): Promise<number>;
|
|
49
50
|
getRow(partition: string, table: string, rowId: string): Promise<StoredRow | undefined>;
|
|
50
51
|
getPushResult(partition: string, clientId: string, clientCommitId: string): Promise<StoredPushResult | undefined>;
|
|
52
|
+
claimReactions(partition: string, query: ReactionClaimQuery): Promise<StoredReaction[]>;
|
|
53
|
+
completeReaction(partition: string, idempotencyKey: string, leaseOwner: string, completedAtMs: number): Promise<boolean>;
|
|
54
|
+
extendReactionLease(partition: string, idempotencyKey: string, leaseOwner: string, leaseExpiresAtMs: number): Promise<boolean>;
|
|
55
|
+
failReaction(partition: string, idempotencyKey: string, update: ReactionFailureUpdate): Promise<boolean>;
|
|
56
|
+
retryReaction(partition: string, idempotencyKey: string, nowMs: number): Promise<boolean>;
|
|
57
|
+
getReaction(partition: string, idempotencyKey: string): Promise<StoredReaction | undefined>;
|
|
58
|
+
listReactions(partition: string, query: ReactionListQuery): Promise<StoredReaction[]>;
|
|
59
|
+
pruneReactions(partition: string, query: ReactionPruneQuery): Promise<PrunedReactionCounts>;
|
|
51
60
|
readCommitWindow(partition: string, query: CommitWindowQuery): Promise<StoredCommit[]>;
|
|
52
61
|
scanRows(partition: string, query: RowScanQuery): Promise<StoredRow[]>;
|
|
53
62
|
scanRowsByIndex(partition: string, query: IndexRowScanQuery): Promise<StoredRow[]>;
|
package/dist/postgres-storage.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { bindAuthoritativePartition, postgresPlaceholders, prepareAuthoritativeQuery, } from './authoritative-query.js';
|
|
1
2
|
import { syncError } from './errors.js';
|
|
2
3
|
import { asBytes, asNumber, } from './pg-executor.js';
|
|
3
4
|
import { commitWindowPageSql, deleteRowSql, dropTableDdl, indexRowPageStatement, layoutsOf, migratePayload, parseLayouts, retiredTableNames, rewritePlan, rewriteRowSql, rewriteValues, SCHEMA_META_DDL_POSTGRES, scanRowPageSql, schemaDdl, selectRowScopesSql, selectRowSql, selectRowsForRewriteSql, upsertSql, upsertValues, } from './relational-rows.js';
|
|
@@ -63,6 +64,26 @@ CREATE TABLE IF NOT EXISTS sync_push_results(
|
|
|
63
64
|
client_commit_id TEXT NOT NULL, result JSONB NOT NULL,
|
|
64
65
|
PRIMARY KEY(partition, client_id, client_commit_id)
|
|
65
66
|
);
|
|
67
|
+
CREATE TABLE IF NOT EXISTS sync_reactions(
|
|
68
|
+
partition TEXT NOT NULL, idempotency_key TEXT NOT NULL,
|
|
69
|
+
type TEXT NOT NULL, version INTEGER NOT NULL, payload JSONB NOT NULL,
|
|
70
|
+
source_client_id TEXT NOT NULL, source_client_commit_id TEXT NOT NULL,
|
|
71
|
+
source_commit_seq BIGINT NOT NULL, created_at_ms BIGINT NOT NULL,
|
|
72
|
+
available_at_ms BIGINT NOT NULL, status TEXT NOT NULL,
|
|
73
|
+
attempts INTEGER NOT NULL, max_attempts INTEGER NOT NULL,
|
|
74
|
+
lease_owner TEXT, lease_expires_at_ms BIGINT, completed_at_ms BIGINT,
|
|
75
|
+
last_failure JSONB,
|
|
76
|
+
PRIMARY KEY(partition, idempotency_key),
|
|
77
|
+
CHECK(status IN ('pending', 'leased', 'completed', 'dead-letter'))
|
|
78
|
+
);
|
|
79
|
+
CREATE INDEX IF NOT EXISTS sync_reactions_due
|
|
80
|
+
ON sync_reactions(partition, status, available_at_ms, created_at_ms, idempotency_key);
|
|
81
|
+
CREATE INDEX IF NOT EXISTS sync_reactions_lease
|
|
82
|
+
ON sync_reactions(partition, status, lease_expires_at_ms);
|
|
83
|
+
CREATE INDEX IF NOT EXISTS sync_reactions_completed
|
|
84
|
+
ON sync_reactions(partition, status, completed_at_ms, idempotency_key);
|
|
85
|
+
CREATE INDEX IF NOT EXISTS sync_reactions_dead_letter
|
|
86
|
+
ON sync_reactions(partition, status, available_at_ms, idempotency_key);
|
|
66
87
|
CREATE TABLE IF NOT EXISTS sync_clients(
|
|
67
88
|
partition TEXT NOT NULL, client_id TEXT NOT NULL, actor_id TEXT NOT NULL,
|
|
68
89
|
cursor BIGINT NOT NULL, subscriptions JSONB NOT NULL,
|
|
@@ -83,6 +104,32 @@ function toBase64(bytes) {
|
|
|
83
104
|
function fromBase64(text) {
|
|
84
105
|
return new Uint8Array(Buffer.from(text, 'base64'));
|
|
85
106
|
}
|
|
107
|
+
function toStoredReaction(record) {
|
|
108
|
+
return {
|
|
109
|
+
idempotencyKey: record.idempotency_key,
|
|
110
|
+
type: record.type,
|
|
111
|
+
version: asNumber(record.version),
|
|
112
|
+
payload: asJson(record.payload),
|
|
113
|
+
sourceClientId: record.source_client_id,
|
|
114
|
+
sourceClientCommitId: record.source_client_commit_id,
|
|
115
|
+
sourceCommitSeq: asNumber(record.source_commit_seq),
|
|
116
|
+
createdAtMs: asNumber(record.created_at_ms),
|
|
117
|
+
maxAttempts: asNumber(record.max_attempts),
|
|
118
|
+
status: record.status,
|
|
119
|
+
attempts: asNumber(record.attempts),
|
|
120
|
+
availableAtMs: asNumber(record.available_at_ms),
|
|
121
|
+
...(record.lease_owner !== null ? { leaseOwner: record.lease_owner } : {}),
|
|
122
|
+
...(record.lease_expires_at_ms !== null
|
|
123
|
+
? { leaseExpiresAtMs: asNumber(record.lease_expires_at_ms) }
|
|
124
|
+
: {}),
|
|
125
|
+
...(record.completed_at_ms !== null
|
|
126
|
+
? { completedAtMs: asNumber(record.completed_at_ms) }
|
|
127
|
+
: {}),
|
|
128
|
+
...(record.last_failure !== null
|
|
129
|
+
? { lastFailure: asJson(record.last_failure) }
|
|
130
|
+
: {}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
86
133
|
/**
|
|
87
134
|
* Serialize a push result to a JSON-able object (stored in a JSONB column).
|
|
88
135
|
* `serverRow` bytes are base64-encoded — JSONB cannot hold raw bytes.
|
|
@@ -191,8 +238,8 @@ function toStoredChange(record) {
|
|
|
191
238
|
};
|
|
192
239
|
}
|
|
193
240
|
/**
|
|
194
|
-
* Migration rewrite
|
|
195
|
-
*
|
|
241
|
+
* Migration rewrite: keyset-paged walk of a row table inside the migration
|
|
242
|
+
* transaction. When `oldLayout` is
|
|
196
243
|
* given every payload re-encodes under the current columns, and the
|
|
197
244
|
* projection (when materialized) refreshes from the payload either way.
|
|
198
245
|
*/
|
|
@@ -425,6 +472,27 @@ class PostgresTransaction {
|
|
|
425
472
|
JSON.stringify(serializePushResult(result)),
|
|
426
473
|
]);
|
|
427
474
|
}
|
|
475
|
+
async enqueueReactions(reactions) {
|
|
476
|
+
this.#assertOpen();
|
|
477
|
+
for (const reaction of reactions) {
|
|
478
|
+
await this.#client.query(`INSERT INTO sync_reactions(
|
|
479
|
+
partition, idempotency_key, type, version, payload,
|
|
480
|
+
source_client_id, source_client_commit_id, source_commit_seq,
|
|
481
|
+
created_at_ms, available_at_ms, status, attempts, max_attempts
|
|
482
|
+
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$9,'pending',0,$10)`, [
|
|
483
|
+
this.#partition,
|
|
484
|
+
reaction.idempotencyKey,
|
|
485
|
+
reaction.type,
|
|
486
|
+
reaction.version,
|
|
487
|
+
JSON.stringify(reaction.payload),
|
|
488
|
+
reaction.sourceClientId,
|
|
489
|
+
reaction.sourceClientCommitId,
|
|
490
|
+
reaction.sourceCommitSeq,
|
|
491
|
+
reaction.createdAtMs,
|
|
492
|
+
reaction.maxAttempts,
|
|
493
|
+
]);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
428
496
|
async commit() {
|
|
429
497
|
this.#assertOpen();
|
|
430
498
|
this.#open = false;
|
|
@@ -599,6 +667,23 @@ export class PostgresServerStorage {
|
|
|
599
667
|
const { rows } = await this.#exec.query('SELECT max_commit_seq FROM sync_partitions WHERE partition=$1', [partition]);
|
|
600
668
|
return rows[0] === undefined ? 0 : asNumber(rows[0].max_commit_seq);
|
|
601
669
|
}
|
|
670
|
+
async queryAuthoritative(partition, query) {
|
|
671
|
+
if (this.#tables === undefined) {
|
|
672
|
+
throw new Error('ensureSchema(schema) must run before registered queries');
|
|
673
|
+
}
|
|
674
|
+
const prepared = bindAuthoritativePartition(prepareAuthoritativeQuery(query.sql, query.params, query.tables, this.#tables), partition);
|
|
675
|
+
return this.#exec.transaction(async (client) => {
|
|
676
|
+
await client.query('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
|
677
|
+
const result = await client.query(postgresPlaceholders(prepared.sql), prepared.params);
|
|
678
|
+
const cursor = await client.query('SELECT max_commit_seq FROM sync_partitions WHERE partition=$1', [partition]);
|
|
679
|
+
return {
|
|
680
|
+
rows: result.rows,
|
|
681
|
+
maxCommitSeq: cursor.rows[0] === undefined
|
|
682
|
+
? 0
|
|
683
|
+
: asNumber(cursor.rows[0].max_commit_seq),
|
|
684
|
+
};
|
|
685
|
+
});
|
|
686
|
+
}
|
|
602
687
|
async getHorizonSeq(partition) {
|
|
603
688
|
const { rows } = await this.#exec.query('SELECT horizon_seq FROM sync_partitions WHERE partition=$1', [partition]);
|
|
604
689
|
return rows[0] === undefined ? 0 : asNumber(rows[0].horizon_seq);
|
|
@@ -624,6 +709,141 @@ export class PostgresServerStorage {
|
|
|
624
709
|
getPushResult(partition, clientId, clientCommitId) {
|
|
625
710
|
return getPushResultOn(this.#exec, partition, clientId, clientCommitId);
|
|
626
711
|
}
|
|
712
|
+
async claimReactions(partition, query) {
|
|
713
|
+
if (query.types.length === 0 || query.limit <= 0)
|
|
714
|
+
return [];
|
|
715
|
+
const typeParams = query.types.map((_, index) => `$${index + 2}`).join(',');
|
|
716
|
+
const nowParam = query.types.length + 2;
|
|
717
|
+
const limitParam = nowParam + 1;
|
|
718
|
+
const workerParam = limitParam + 1;
|
|
719
|
+
const expiresParam = workerParam + 1;
|
|
720
|
+
const { rows } = await this.#exec.query(`WITH due AS (
|
|
721
|
+
SELECT partition, idempotency_key
|
|
722
|
+
FROM sync_reactions
|
|
723
|
+
WHERE partition=$1 AND type IN (${typeParams})
|
|
724
|
+
AND ((status='pending' AND available_at_ms<=$${nowParam})
|
|
725
|
+
OR (status='leased' AND lease_expires_at_ms<=$${nowParam}))
|
|
726
|
+
ORDER BY CASE WHEN status='leased' THEN lease_expires_at_ms
|
|
727
|
+
ELSE available_at_ms END,
|
|
728
|
+
created_at_ms, idempotency_key
|
|
729
|
+
FOR UPDATE SKIP LOCKED
|
|
730
|
+
LIMIT $${limitParam}
|
|
731
|
+
)
|
|
732
|
+
UPDATE sync_reactions AS reaction
|
|
733
|
+
SET status='leased', attempts=reaction.attempts+1,
|
|
734
|
+
lease_owner=$${workerParam}, lease_expires_at_ms=$${expiresParam},
|
|
735
|
+
completed_at_ms=NULL
|
|
736
|
+
FROM due
|
|
737
|
+
WHERE reaction.partition=due.partition
|
|
738
|
+
AND reaction.idempotency_key=due.idempotency_key
|
|
739
|
+
RETURNING reaction.*`, [
|
|
740
|
+
partition,
|
|
741
|
+
...query.types,
|
|
742
|
+
query.nowMs,
|
|
743
|
+
query.limit,
|
|
744
|
+
query.leaseOwner,
|
|
745
|
+
Math.min(Number.MAX_SAFE_INTEGER, query.nowMs + query.leaseDurationMs),
|
|
746
|
+
]);
|
|
747
|
+
return rows
|
|
748
|
+
.map(toStoredReaction)
|
|
749
|
+
.sort((a, b) => a.createdAtMs - b.createdAtMs ||
|
|
750
|
+
a.idempotencyKey.localeCompare(b.idempotencyKey));
|
|
751
|
+
}
|
|
752
|
+
async completeReaction(partition, idempotencyKey, leaseOwner, completedAtMs) {
|
|
753
|
+
const result = await this.#exec.query(`UPDATE sync_reactions
|
|
754
|
+
SET status='completed', completed_at_ms=$4,
|
|
755
|
+
lease_owner=NULL, lease_expires_at_ms=NULL
|
|
756
|
+
WHERE partition=$1 AND idempotency_key=$2
|
|
757
|
+
AND status='leased' AND lease_owner=$3`, [partition, idempotencyKey, leaseOwner, completedAtMs]);
|
|
758
|
+
return result.rowCount === 1;
|
|
759
|
+
}
|
|
760
|
+
async extendReactionLease(partition, idempotencyKey, leaseOwner, leaseExpiresAtMs) {
|
|
761
|
+
const result = await this.#exec.query(`UPDATE sync_reactions SET lease_expires_at_ms=$4
|
|
762
|
+
WHERE partition=$1 AND idempotency_key=$2
|
|
763
|
+
AND status='leased' AND lease_owner=$3`, [partition, idempotencyKey, leaseOwner, leaseExpiresAtMs]);
|
|
764
|
+
return result.rowCount === 1;
|
|
765
|
+
}
|
|
766
|
+
async failReaction(partition, idempotencyKey, update) {
|
|
767
|
+
const result = await this.#exec.query(`UPDATE sync_reactions
|
|
768
|
+
SET status=$4, available_at_ms=$5, last_failure=$6,
|
|
769
|
+
lease_owner=NULL, lease_expires_at_ms=NULL
|
|
770
|
+
WHERE partition=$1 AND idempotency_key=$2
|
|
771
|
+
AND status='leased' AND lease_owner=$3`, [
|
|
772
|
+
partition,
|
|
773
|
+
idempotencyKey,
|
|
774
|
+
update.leaseOwner,
|
|
775
|
+
update.retryAtMs === undefined ? 'dead-letter' : 'pending',
|
|
776
|
+
update.retryAtMs ?? update.failure.atMs,
|
|
777
|
+
JSON.stringify(update.failure),
|
|
778
|
+
]);
|
|
779
|
+
return result.rowCount === 1;
|
|
780
|
+
}
|
|
781
|
+
async retryReaction(partition, idempotencyKey, nowMs) {
|
|
782
|
+
const result = await this.#exec.query(`UPDATE sync_reactions
|
|
783
|
+
SET status='pending', attempts=0, available_at_ms=$3,
|
|
784
|
+
last_failure=NULL, lease_owner=NULL, lease_expires_at_ms=NULL,
|
|
785
|
+
completed_at_ms=NULL
|
|
786
|
+
WHERE partition=$1 AND idempotency_key=$2 AND status='dead-letter'`, [partition, idempotencyKey, nowMs]);
|
|
787
|
+
return result.rowCount === 1;
|
|
788
|
+
}
|
|
789
|
+
async getReaction(partition, idempotencyKey) {
|
|
790
|
+
const { rows } = await this.#exec.query('SELECT * FROM sync_reactions WHERE partition=$1 AND idempotency_key=$2', [partition, idempotencyKey]);
|
|
791
|
+
return rows[0] === undefined ? undefined : toStoredReaction(rows[0]);
|
|
792
|
+
}
|
|
793
|
+
async listReactions(partition, query) {
|
|
794
|
+
const where = ['partition=$1'];
|
|
795
|
+
const params = [partition];
|
|
796
|
+
if (query.statuses !== undefined && query.statuses.length > 0) {
|
|
797
|
+
const placeholders = query.statuses.map((_, index) => `$${params.length + index + 1}`);
|
|
798
|
+
where.push(`status IN (${placeholders.join(',')})`);
|
|
799
|
+
params.push(...query.statuses);
|
|
800
|
+
}
|
|
801
|
+
if (query.types !== undefined && query.types.length > 0) {
|
|
802
|
+
const placeholders = query.types.map((_, index) => `$${params.length + index + 1}`);
|
|
803
|
+
where.push(`type IN (${placeholders.join(',')})`);
|
|
804
|
+
params.push(...query.types);
|
|
805
|
+
}
|
|
806
|
+
params.push(query.limit);
|
|
807
|
+
const { rows } = await this.#exec.query(`SELECT * FROM sync_reactions WHERE ${where.join(' AND ')}
|
|
808
|
+
ORDER BY created_at_ms DESC, idempotency_key DESC LIMIT $${params.length}`, params);
|
|
809
|
+
return rows.map(toStoredReaction);
|
|
810
|
+
}
|
|
811
|
+
async pruneReactions(partition, query) {
|
|
812
|
+
if (query.limit <= 0)
|
|
813
|
+
return { completed: 0, deadLetter: 0 };
|
|
814
|
+
const { rows } = await this.#exec.query(`WITH targets AS (
|
|
815
|
+
SELECT partition, idempotency_key
|
|
816
|
+
FROM sync_reactions
|
|
817
|
+
WHERE partition=$1
|
|
818
|
+
AND ((status='completed' AND completed_at_ms IS NOT NULL
|
|
819
|
+
AND completed_at_ms<$2)
|
|
820
|
+
OR (status='dead-letter' AND available_at_ms<$3))
|
|
821
|
+
ORDER BY CASE WHEN status='completed' THEN completed_at_ms
|
|
822
|
+
ELSE available_at_ms END,
|
|
823
|
+
idempotency_key
|
|
824
|
+
FOR UPDATE SKIP LOCKED
|
|
825
|
+
LIMIT $4
|
|
826
|
+
)
|
|
827
|
+
DELETE FROM sync_reactions AS reaction
|
|
828
|
+
USING targets
|
|
829
|
+
WHERE reaction.partition=targets.partition
|
|
830
|
+
AND reaction.idempotency_key=targets.idempotency_key
|
|
831
|
+
AND ((reaction.status='completed'
|
|
832
|
+
AND reaction.completed_at_ms IS NOT NULL
|
|
833
|
+
AND reaction.completed_at_ms<$2)
|
|
834
|
+
OR (reaction.status='dead-letter'
|
|
835
|
+
AND reaction.available_at_ms<$3))
|
|
836
|
+
RETURNING reaction.status`, [
|
|
837
|
+
partition,
|
|
838
|
+
query.completedBeforeMs,
|
|
839
|
+
query.deadLetterBeforeMs,
|
|
840
|
+
query.limit,
|
|
841
|
+
]);
|
|
842
|
+
return {
|
|
843
|
+
completed: rows.filter((row) => row.status === 'completed').length,
|
|
844
|
+
deadLetter: rows.filter((row) => row.status === 'dead-letter').length,
|
|
845
|
+
};
|
|
846
|
+
}
|
|
627
847
|
async readCommitWindow(partition, query) {
|
|
628
848
|
const variables = Object.keys(query.scopeFilter).sort();
|
|
629
849
|
const firstVariable = variables[0];
|
|
@@ -818,7 +1038,7 @@ export class PostgresServerStorage {
|
|
|
818
1038
|
const { rows } = await this.#exec.query('SELECT DISTINCT blob_id FROM sync_blob_refs WHERE partition=$1', [partition]);
|
|
819
1039
|
return rows.map((r) => r.blob_id);
|
|
820
1040
|
}
|
|
821
|
-
// -- admin/console read surface
|
|
1041
|
+
// -- admin/console read surface --------------------------------------------
|
|
822
1042
|
async listClientRecords(partition) {
|
|
823
1043
|
const { rows } = await this.#exec.query('SELECT client_id, actor_id, cursor, subscriptions, updated_at_ms FROM sync_clients WHERE partition=$1 ORDER BY updated_at_ms DESC', [partition]);
|
|
824
1044
|
return rows.map((r) => ({
|
package/dist/pull.js
CHANGED
|
@@ -170,7 +170,7 @@ async function* sqliteImageSegment(ctx, schema, limits, plan, asOf, digest, trac
|
|
|
170
170
|
// builder through `sqliteImageBuilder`; when omitted we default to the
|
|
171
171
|
// in-tree `buildSqliteImage` ONLY on a Bun runtime, reached by a *dynamic*
|
|
172
172
|
// import so `bun:sqlite` is never a static dependency of the pull path
|
|
173
|
-
// (
|
|
173
|
+
// (runtime neutrality is enforced by test/runtime-neutrality.test.ts). On
|
|
174
174
|
// Workers/edge (no `Bun`) this yields the rows lane for a bit-2 client — a
|
|
175
175
|
// support floor, not a fallback (§5.3: sqlite is an *accept*, not a demand).
|
|
176
176
|
const buildImage = await resolveImageBuilder(ctx);
|
package/dist/push.d.ts
CHANGED
|
@@ -17,11 +17,11 @@
|
|
|
17
17
|
* merge); a validator throw rejects the whole commit atomically with a
|
|
18
18
|
* host code.
|
|
19
19
|
*/
|
|
20
|
-
import { type PushCommitFrame, type PushResultFrame } from '@syncular/core';
|
|
20
|
+
import { type PushCommitFrame, type PushOperation, type PushResultFrame } from '@syncular/core';
|
|
21
21
|
import type { SyncRequestContext } from './context.js';
|
|
22
22
|
import type { CompiledSchema } from './schema.js';
|
|
23
23
|
import type { ResolvedScopes } from './scopes.js';
|
|
24
|
-
import type { StoredCommit } from './storage.js';
|
|
24
|
+
import type { StorageTransaction, StoredCommit } from './storage.js';
|
|
25
25
|
export interface ProcessedPushCommit {
|
|
26
26
|
readonly frame: PushResultFrame;
|
|
27
27
|
/** True when this request observed an already-recorded idempotency outcome. */
|
|
@@ -43,3 +43,9 @@ export declare function processPushCommit(ctx: SyncRequestContext, schema: Compi
|
|
|
43
43
|
* cache provenance needed by structured events and server helpers.
|
|
44
44
|
*/
|
|
45
45
|
export declare function processPushCommitWithTrace(ctx: SyncRequestContext, schema: CompiledSchema, resolved: ResolvedScopes, clientId: string, frame: PushCommitFrame): Promise<ProcessedPushCommit>;
|
|
46
|
+
/**
|
|
47
|
+
* Shared serialized apply path for SSP2 commits and authoritative commands.
|
|
48
|
+
* The builder runs after the partition lock and idempotency re-check, so its
|
|
49
|
+
* reads and the returned operations share the transaction that is committed.
|
|
50
|
+
*/
|
|
51
|
+
export declare function processPushOperationsWithTrace(ctx: SyncRequestContext, schema: CompiledSchema, resolved: ResolvedScopes, clientId: string, clientCommitId: string, buildOperations: (tx: StorageTransaction) => Promise<readonly PushOperation[]>): Promise<ProcessedPushCommit>;
|
package/dist/push.js
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
import { decodeRow, encodeRow, parseBlobRef, } from '@syncular/core';
|
|
21
21
|
import { clockOf } from './context.js';
|
|
22
22
|
import { SyncError } from './errors.js';
|
|
23
|
+
import { emitEvent } from './events.js';
|
|
24
|
+
import { prepareReactions, toNewReactions, } from './reactions.js';
|
|
23
25
|
import { authorizeWrite, renderScopeValue, storedScopesForRow } from './scopes.js';
|
|
24
26
|
import { StorageConstraintError } from './storage-errors.js';
|
|
25
27
|
import { CommitValidationRejection, toValidateRow, ValidationRejection, } from './validate.js';
|
|
@@ -514,10 +516,18 @@ export async function processPushCommit(ctx, schema, resolved, clientId, frame)
|
|
|
514
516
|
* cache provenance needed by structured events and server helpers.
|
|
515
517
|
*/
|
|
516
518
|
export async function processPushCommitWithTrace(ctx, schema, resolved, clientId, frame) {
|
|
519
|
+
return processPushOperationsWithTrace(ctx, schema, resolved, clientId, frame.clientCommitId, async () => frame.operations);
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Shared serialized apply path for SSP2 commits and authoritative commands.
|
|
523
|
+
* The builder runs after the partition lock and idempotency re-check, so its
|
|
524
|
+
* reads and the returned operations share the transaction that is committed.
|
|
525
|
+
*/
|
|
526
|
+
export async function processPushOperationsWithTrace(ctx, schema, resolved, clientId, clientCommitId, buildOperations) {
|
|
517
527
|
const { storage, partition } = ctx;
|
|
518
528
|
let persisted;
|
|
519
529
|
try {
|
|
520
|
-
persisted = await storage.getPushResult(partition, clientId,
|
|
530
|
+
persisted = await storage.getPushResult(partition, clientId, clientCommitId);
|
|
521
531
|
}
|
|
522
532
|
catch (error) {
|
|
523
533
|
if (error instanceof SyncError &&
|
|
@@ -525,28 +535,30 @@ export async function processPushCommitWithTrace(ctx, schema, resolved, clientId
|
|
|
525
535
|
// §6.3: answer the retryable cache-miss for this commit rather than
|
|
526
536
|
// re-applying. Not persisted — a retry may find a readable record.
|
|
527
537
|
return {
|
|
528
|
-
frame: idempotencyCacheMissFrame(
|
|
538
|
+
frame: idempotencyCacheMissFrame(clientCommitId, error),
|
|
529
539
|
replayed: false,
|
|
530
540
|
};
|
|
531
541
|
}
|
|
532
542
|
throw error;
|
|
533
543
|
}
|
|
534
544
|
if (persisted !== undefined) {
|
|
535
|
-
return processedPushCommit(
|
|
545
|
+
return processedPushCommit(clientCommitId, persisted, true);
|
|
536
546
|
}
|
|
537
547
|
const createdAtMs = clockOf(ctx)();
|
|
538
548
|
const blobCtx = { store: ctx.blobs, partition };
|
|
539
549
|
const crdtMergers = ctx.crdtMergers;
|
|
540
550
|
const validators = ctx.validators;
|
|
541
551
|
const commitValidator = ctx.commitValidator;
|
|
552
|
+
const reactionPlanner = ctx.reactionPlanner;
|
|
542
553
|
const tx = await storage.begin(partition);
|
|
543
554
|
const lockPartitionForPush = tx.lockPartitionForPush?.bind(tx) ??
|
|
544
555
|
tx.lockPartitionForCommitValidation?.bind(tx);
|
|
545
556
|
const commitRejectedPushResult = tx.commitRejectedPushResult?.bind(tx);
|
|
546
557
|
try {
|
|
547
558
|
if (lockPartitionForPush === undefined ||
|
|
548
|
-
commitRejectedPushResult === undefined
|
|
549
|
-
|
|
559
|
+
commitRejectedPushResult === undefined ||
|
|
560
|
+
(reactionPlanner !== undefined && tx.enqueueReactions === undefined)) {
|
|
561
|
+
throw new Error('storage transaction does not support serialized push apply, atomic rejection finalization, and configured durable reactions');
|
|
550
562
|
}
|
|
551
563
|
await lockPartitionForPush();
|
|
552
564
|
// The optimistic lookup above may have raced another delivery. Re-check
|
|
@@ -559,11 +571,11 @@ export async function processPushCommitWithTrace(ctx, schema, resolved, clientId
|
|
|
559
571
|
// transaction-scoped read observes it.
|
|
560
572
|
try {
|
|
561
573
|
const serializedPersisted = tx.getPushResult !== undefined
|
|
562
|
-
? await tx.getPushResult(clientId,
|
|
563
|
-
: await storage.getPushResult(partition, clientId,
|
|
574
|
+
? await tx.getPushResult(clientId, clientCommitId)
|
|
575
|
+
: await storage.getPushResult(partition, clientId, clientCommitId);
|
|
564
576
|
if (serializedPersisted !== undefined) {
|
|
565
577
|
await tx.rollback();
|
|
566
|
-
return processedPushCommit(
|
|
578
|
+
return processedPushCommit(clientCommitId, serializedPersisted, true);
|
|
567
579
|
}
|
|
568
580
|
}
|
|
569
581
|
catch (error) {
|
|
@@ -571,7 +583,7 @@ export async function processPushCommitWithTrace(ctx, schema, resolved, clientId
|
|
|
571
583
|
error.code === 'sync.idempotency_cache_miss') {
|
|
572
584
|
await tx.rollback();
|
|
573
585
|
return {
|
|
574
|
-
frame: idempotencyCacheMissFrame(
|
|
586
|
+
frame: idempotencyCacheMissFrame(clientCommitId, error),
|
|
575
587
|
replayed: false,
|
|
576
588
|
};
|
|
577
589
|
}
|
|
@@ -581,8 +593,9 @@ export async function processPushCommitWithTrace(ctx, schema, resolved, clientId
|
|
|
581
593
|
const changes = [];
|
|
582
594
|
const validatedOperations = [];
|
|
583
595
|
let terminated;
|
|
584
|
-
|
|
585
|
-
|
|
596
|
+
const operations = await buildOperations(tx);
|
|
597
|
+
for (let opIndex = 0; opIndex < operations.length; opIndex++) {
|
|
598
|
+
const op = operations[opIndex];
|
|
586
599
|
if (op === undefined)
|
|
587
600
|
continue;
|
|
588
601
|
const outcome = await applyOperation(tx, schema, resolved, op, opIndex, blobCtx, crdtMergers, validators, partition, ctx.actorId);
|
|
@@ -596,11 +609,22 @@ export async function processPushCommitWithTrace(ctx, schema, resolved, clientId
|
|
|
596
609
|
changes.push(outcome.change);
|
|
597
610
|
}
|
|
598
611
|
if (terminated === undefined) {
|
|
599
|
-
const commitReject = await runCommitValidator(commitValidator, tx, schema, clientId,
|
|
612
|
+
const commitReject = await runCommitValidator(commitValidator, tx, schema, clientId, clientCommitId, ctx.actorId, partition, validatedOperations);
|
|
600
613
|
if (commitReject?.kind === 'terminate') {
|
|
601
614
|
terminated = commitReject.record;
|
|
602
615
|
}
|
|
603
616
|
}
|
|
617
|
+
let preparedReactions = [];
|
|
618
|
+
if (terminated === undefined && reactionPlanner !== undefined) {
|
|
619
|
+
preparedReactions = await prepareReactions(reactionPlanner, {
|
|
620
|
+
clientId,
|
|
621
|
+
clientCommitId,
|
|
622
|
+
actorId: ctx.actorId,
|
|
623
|
+
partition,
|
|
624
|
+
operations: validatedOperations,
|
|
625
|
+
read: commitValidationReader(tx, schema),
|
|
626
|
+
});
|
|
627
|
+
}
|
|
604
628
|
if (terminated !== undefined) {
|
|
605
629
|
// §6.3 rejected: only the terminating operation's record; §6.4:
|
|
606
630
|
// every write of the commit rolls back.
|
|
@@ -610,27 +634,57 @@ export async function processPushCommitWithTrace(ctx, schema, resolved, clientId
|
|
|
610
634
|
});
|
|
611
635
|
// Discard candidates and persist the rejection while retaining the same
|
|
612
636
|
// partition lock. There is no unlock gap in which a duplicate can rerun.
|
|
613
|
-
await commitRejectedPushResult(clientId,
|
|
614
|
-
const canonical = await storage.getPushResult(partition, clientId,
|
|
637
|
+
await commitRejectedPushResult(clientId, clientCommitId, stored);
|
|
638
|
+
const canonical = await storage.getPushResult(partition, clientId, clientCommitId);
|
|
615
639
|
if (canonical === undefined) {
|
|
616
640
|
throw new Error('push rejection finalization did not persist an outcome');
|
|
617
641
|
}
|
|
618
|
-
return processedPushCommit(
|
|
642
|
+
return processedPushCommit(clientCommitId, canonical, canonical.cacheIdentity !== stored.cacheIdentity);
|
|
619
643
|
}
|
|
620
644
|
const commitSeq = await tx.appendCommit({
|
|
621
645
|
clientId,
|
|
622
|
-
clientCommitId
|
|
646
|
+
clientCommitId,
|
|
623
647
|
actorId: ctx.actorId,
|
|
624
648
|
createdAtMs,
|
|
625
649
|
changes,
|
|
626
650
|
});
|
|
651
|
+
const newReactions = toNewReactions(preparedReactions, {
|
|
652
|
+
clientId,
|
|
653
|
+
clientCommitId,
|
|
654
|
+
commitSeq,
|
|
655
|
+
createdAtMs,
|
|
656
|
+
});
|
|
657
|
+
if (newReactions.length > 0) {
|
|
658
|
+
const enqueueReactions = tx.enqueueReactions;
|
|
659
|
+
if (enqueueReactions === undefined) {
|
|
660
|
+
throw new Error('storage lost durable reaction enqueue support');
|
|
661
|
+
}
|
|
662
|
+
await enqueueReactions.call(tx, newReactions);
|
|
663
|
+
}
|
|
627
664
|
const stored = newStoredPushResult(createdAtMs, {
|
|
628
665
|
status: 'applied',
|
|
629
666
|
commitSeq,
|
|
630
667
|
results,
|
|
631
668
|
});
|
|
632
|
-
await tx.putPushResult(clientId,
|
|
669
|
+
await tx.putPushResult(clientId, clientCommitId, stored);
|
|
633
670
|
await tx.commit();
|
|
671
|
+
if (ctx.events !== undefined) {
|
|
672
|
+
const atMs = clockOf(ctx)();
|
|
673
|
+
for (const reaction of newReactions) {
|
|
674
|
+
emitEvent(ctx.events, {
|
|
675
|
+
type: 'reaction.queued',
|
|
676
|
+
atMs,
|
|
677
|
+
partition,
|
|
678
|
+
actorId: ctx.actorId,
|
|
679
|
+
clientId,
|
|
680
|
+
clientCommitId,
|
|
681
|
+
commitSeq,
|
|
682
|
+
idempotencyKey: reaction.idempotencyKey,
|
|
683
|
+
reactionType: reaction.type,
|
|
684
|
+
version: reaction.version,
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
}
|
|
634
688
|
if (ctx.realtime !== undefined && changes.length > 0) {
|
|
635
689
|
await ctx.realtime.notifyCommit(partition, {
|
|
636
690
|
commitSeq,
|
|
@@ -639,7 +693,7 @@ export async function processPushCommitWithTrace(ctx, schema, resolved, clientId
|
|
|
639
693
|
changes,
|
|
640
694
|
});
|
|
641
695
|
}
|
|
642
|
-
return processedPushCommit(
|
|
696
|
+
return processedPushCommit(clientCommitId, stored, false);
|
|
643
697
|
}
|
|
644
698
|
catch (error) {
|
|
645
699
|
if (error instanceof StorageConstraintError) {
|
|
@@ -660,12 +714,12 @@ export async function processPushCommitWithTrace(ctx, schema, resolved, clientId
|
|
|
660
714
|
throw new Error('storage transaction lost atomic push rejection finalization support');
|
|
661
715
|
}
|
|
662
716
|
try {
|
|
663
|
-
await commitRejectedPushResult(clientId,
|
|
664
|
-
const canonical = await storage.getPushResult(partition, clientId,
|
|
717
|
+
await commitRejectedPushResult(clientId, clientCommitId, stored);
|
|
718
|
+
const canonical = await storage.getPushResult(partition, clientId, clientCommitId);
|
|
665
719
|
if (canonical === undefined) {
|
|
666
720
|
throw new Error('push rejection finalization did not persist an outcome');
|
|
667
721
|
}
|
|
668
|
-
return processedPushCommit(
|
|
722
|
+
return processedPushCommit(clientCommitId, canonical, canonical.cacheIdentity !== stored.cacheIdentity);
|
|
669
723
|
}
|
|
670
724
|
catch (finalizationError) {
|
|
671
725
|
// A failed finalization must still release the transaction: on
|