@abloatai/humans 0.59.0 → 0.59.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/dist/local/interfaces/index.d.ts +1 -1
- package/dist/local/transactions/mutations/batchProcessing.js +5 -1
- package/dist/local/transactions/mutations/commitPayload.d.ts +2 -0
- package/dist/local/transactions/mutations/commitPayload.js +14 -0
- package/dist/local/transactions/mutations/pendingDrain.js +5 -1
- package/dist/local/transactions/mutations/replayValidation.d.ts +36 -0
- package/dist/local/transactions/mutations/replayValidation.js +2 -0
- package/package.json +2 -2
- package/src/local/interfaces/index.ts +1 -1
- package/src/local/transactions/mutations/batchProcessing.ts +5 -1
- package/src/local/transactions/mutations/commitPayload.ts +17 -0
- package/src/local/transactions/mutations/pendingDrain.ts +5 -1
- package/src/local/transactions/mutations/replayValidation.ts +2 -0
|
@@ -141,7 +141,7 @@ import type { MutationOptions } from '@abloatai/transaction/client/resources/mut
|
|
|
141
141
|
* `claim` are deliberately absent: both are resolved on the client before a write
|
|
142
142
|
* is staged, so neither reaches this layer.
|
|
143
143
|
*/
|
|
144
|
-
export type WriteOptions = Pick<MutationOptions, 'readAt' | 'idempotencyKey' | 'label' | 'fenceToken' | 'claimRef'>;
|
|
144
|
+
export type WriteOptions = Pick<MutationOptions, 'readAt' | 'reads' | 'idempotencyKey' | 'label' | 'fenceToken' | 'claimRef'>;
|
|
145
145
|
/** A single mutation within a batch. Its `options` travel with it so the server
|
|
146
146
|
* can cache and replay the operation for idempotent retries. */
|
|
147
147
|
export interface MutationOperation {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AbloError, AbloNotFoundError } from '@abloatai/transaction/errors';
|
|
2
|
-
import { applyWriteOptions, normalizeModelKey, TX_TYPE_TO_MUTATION_OP } from './commitPayload.js';
|
|
2
|
+
import { applyWriteOptions, collectQueuedReads, normalizeModelKey, TX_TYPE_TO_MUTATION_OP } from './commitPayload.js';
|
|
3
3
|
export async function processBatch(ctx) {
|
|
4
4
|
if (ctx.durableReplayBlock)
|
|
5
5
|
return;
|
|
@@ -69,6 +69,7 @@ export async function processBatch(ctx) {
|
|
|
69
69
|
origin: 'model_batch',
|
|
70
70
|
operations: batchOps.map(({ op }) => op),
|
|
71
71
|
sourceMutationIds: ctx.sourceMutationIdsFor(batch),
|
|
72
|
+
commitOptions: { reads: collectQueuedReads(batch) },
|
|
72
73
|
createdAt: Math.min(...batch.map((transaction) => transaction.createdAt)),
|
|
73
74
|
sealedAt: batch[0]?.commitEnvelope?.sealedAt ?? Date.now(),
|
|
74
75
|
sequence: batch[0]?.commitEnvelope?.sequence,
|
|
@@ -84,6 +85,9 @@ export async function processBatch(ctx) {
|
|
|
84
85
|
dispatchStarted = true;
|
|
85
86
|
const result = ctx.parseMutationCommitResult(await ctx.dispatchCommitBounded(operations, {
|
|
86
87
|
idempotencyKey: commitIdempotencyKey,
|
|
88
|
+
...(durableEnvelope.commitOptions.reads !== undefined
|
|
89
|
+
? { reads: durableEnvelope.commitOptions.reads }
|
|
90
|
+
: {}),
|
|
87
91
|
}));
|
|
88
92
|
await ctx.persistDurableCommitAcceptance(durableEnvelope, result);
|
|
89
93
|
const lastSyncId = result.lastSyncId;
|
|
@@ -105,6 +105,8 @@ export interface QueuedMutation {
|
|
|
105
105
|
*/
|
|
106
106
|
confirmation?: Promise<void>;
|
|
107
107
|
}
|
|
108
|
+
/** Merge per-write premises into the one batch-level read set sent on wire. */
|
|
109
|
+
export declare function collectQueuedReads(transactions: readonly QueuedMutation[]): MutationOptions['reads'] | undefined;
|
|
108
110
|
export declare const normalizeModelKey: (modelName: string) => string;
|
|
109
111
|
export declare const stripModelSuffix: (modelName: string) => string;
|
|
110
112
|
/**
|
|
@@ -75,6 +75,20 @@ export function projectCommitPayload(modelName, source, opts, runtime = globalRu
|
|
|
75
75
|
}
|
|
76
76
|
return snapshotJsonValue(out, '$.input');
|
|
77
77
|
}
|
|
78
|
+
/** Merge per-write premises into the one batch-level read set sent on wire. */
|
|
79
|
+
export function collectQueuedReads(transactions) {
|
|
80
|
+
const declared = transactions
|
|
81
|
+
.map((transaction) => transaction.writeOptions?.reads)
|
|
82
|
+
.filter((reads) => reads !== undefined);
|
|
83
|
+
if (declared.length === 0)
|
|
84
|
+
return undefined;
|
|
85
|
+
const unique = new Map();
|
|
86
|
+
for (const dependency of declared.flatMap((reads) => reads ?? [])) {
|
|
87
|
+
unique.set(JSON.stringify(dependency), dependency);
|
|
88
|
+
}
|
|
89
|
+
const reads = [...unique.values()];
|
|
90
|
+
return reads.length > 0 ? reads : null;
|
|
91
|
+
}
|
|
78
92
|
export const normalizeModelKey = (modelName) => modelName.replace('Model', '').toLowerCase();
|
|
79
93
|
export const stripModelSuffix = (modelName) => modelName.replace('Model', '');
|
|
80
94
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { applyWriteOptions, TX_TYPE_TO_MUTATION_OP } from './commitPayload.js';
|
|
1
|
+
import { applyWriteOptions, collectQueuedReads, TX_TYPE_TO_MUTATION_OP } from './commitPayload.js';
|
|
2
2
|
export async function drainPendingConfirmations(ctx) {
|
|
3
3
|
ctx.assertDurableReplayOpen();
|
|
4
4
|
// Kick the commit lane too: atomic envelopes from `commits.create()` may
|
|
@@ -39,6 +39,7 @@ export async function drainPendingConfirmations(ctx) {
|
|
|
39
39
|
origin: 'model_batch',
|
|
40
40
|
operations: projectedOperations,
|
|
41
41
|
sourceMutationIds: ctx.sourceMutationIdsFor(batch),
|
|
42
|
+
commitOptions: { reads: collectQueuedReads(batch) },
|
|
42
43
|
createdAt: Math.min(...batch.map((transaction) => transaction.createdAt)),
|
|
43
44
|
sealedAt: batch[0]?.commitEnvelope?.sealedAt ?? Date.now(),
|
|
44
45
|
sequence: batch[0]?.commitEnvelope?.sequence,
|
|
@@ -46,6 +47,9 @@ export async function drainPendingConfirmations(ctx) {
|
|
|
46
47
|
ctx.assertEnvelopeInsideReplayWindow(durableEnvelope);
|
|
47
48
|
const result = ctx.parseMutationCommitResult(await ctx.dispatchCommitBounded(durableEnvelope.operations, {
|
|
48
49
|
idempotencyKey,
|
|
50
|
+
...(durableEnvelope.commitOptions.reads !== undefined
|
|
51
|
+
? { reads: durableEnvelope.commitOptions.reads }
|
|
52
|
+
: {}),
|
|
49
53
|
}));
|
|
50
54
|
await ctx.persistDurableCommitAcceptance(durableEnvelope, result);
|
|
51
55
|
if (result.status === 'queued') {
|
|
@@ -56,6 +56,15 @@ export declare const persistedTransactionSchema: z.ZodObject<{
|
|
|
56
56
|
sourceMutationIds: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
57
57
|
writeOptions: z.ZodOptional<z.ZodObject<{
|
|
58
58
|
readAt: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
59
|
+
reads: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
60
|
+
model: z.ZodString;
|
|
61
|
+
id: z.ZodString;
|
|
62
|
+
fields: z.ZodOptional<z.ZodReadonly<z.ZodArray<z.ZodString>>>;
|
|
63
|
+
readAt: z.ZodNumber;
|
|
64
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
65
|
+
group: z.ZodTemplateLiteral<`${string}:${string}`>;
|
|
66
|
+
readAt: z.ZodNumber;
|
|
67
|
+
}, z.core.$strip>]>>>>;
|
|
59
68
|
idempotencyKey: z.ZodOptional<z.ZodString>;
|
|
60
69
|
label: z.ZodOptional<z.ZodString>;
|
|
61
70
|
fenceToken: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
@@ -97,6 +106,15 @@ export declare const persistedMutationSchema: z.ZodObject<{
|
|
|
97
106
|
capturedChanges: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
98
107
|
writeOptions: z.ZodOptional<z.ZodObject<{
|
|
99
108
|
readAt: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
109
|
+
reads: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
110
|
+
model: z.ZodString;
|
|
111
|
+
id: z.ZodString;
|
|
112
|
+
fields: z.ZodOptional<z.ZodReadonly<z.ZodArray<z.ZodString>>>;
|
|
113
|
+
readAt: z.ZodNumber;
|
|
114
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
115
|
+
group: z.ZodTemplateLiteral<`${string}:${string}`>;
|
|
116
|
+
readAt: z.ZodNumber;
|
|
117
|
+
}, z.core.$strip>]>>>>;
|
|
100
118
|
idempotencyKey: z.ZodOptional<z.ZodString>;
|
|
101
119
|
label: z.ZodOptional<z.ZodString>;
|
|
102
120
|
fenceToken: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
@@ -131,6 +149,15 @@ export declare const legacyPendingMutationRecordSchema: z.ZodObject<{
|
|
|
131
149
|
capturedChanges: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
132
150
|
writeOptions: z.ZodOptional<z.ZodObject<{
|
|
133
151
|
readAt: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
152
|
+
reads: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
153
|
+
model: z.ZodString;
|
|
154
|
+
id: z.ZodString;
|
|
155
|
+
fields: z.ZodOptional<z.ZodReadonly<z.ZodArray<z.ZodString>>>;
|
|
156
|
+
readAt: z.ZodNumber;
|
|
157
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
158
|
+
group: z.ZodTemplateLiteral<`${string}:${string}`>;
|
|
159
|
+
readAt: z.ZodNumber;
|
|
160
|
+
}, z.core.$strip>]>>>>;
|
|
134
161
|
idempotencyKey: z.ZodOptional<z.ZodString>;
|
|
135
162
|
label: z.ZodOptional<z.ZodString>;
|
|
136
163
|
fenceToken: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
@@ -164,6 +191,15 @@ export declare const pendingMutationRecordSchema: z.ZodObject<{
|
|
|
164
191
|
capturedChanges: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
165
192
|
writeOptions: z.ZodOptional<z.ZodObject<{
|
|
166
193
|
readAt: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
194
|
+
reads: z.ZodOptional<z.ZodNullable<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
|
|
195
|
+
model: z.ZodString;
|
|
196
|
+
id: z.ZodString;
|
|
197
|
+
fields: z.ZodOptional<z.ZodReadonly<z.ZodArray<z.ZodString>>>;
|
|
198
|
+
readAt: z.ZodNumber;
|
|
199
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
200
|
+
group: z.ZodTemplateLiteral<`${string}:${string}`>;
|
|
201
|
+
readAt: z.ZodNumber;
|
|
202
|
+
}, z.core.$strip>]>>>>;
|
|
167
203
|
idempotencyKey: z.ZodOptional<z.ZodString>;
|
|
168
204
|
label: z.ZodOptional<z.ZodString>;
|
|
169
205
|
fenceToken: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
@@ -18,10 +18,12 @@ import { z } from 'zod';
|
|
|
18
18
|
import { computePriorityScore, normalizeModelKey } from './commitPayload.js';
|
|
19
19
|
import { globalRuntime } from '../../context.js';
|
|
20
20
|
import { commitEnvelopeMemberSchema, commitOutboxScopeSchema, } from '@abloatai/transaction/commit';
|
|
21
|
+
import { readDependencySchema } from '@abloatai/transaction/coordination/schema';
|
|
21
22
|
/** The subset of a write's options that is stored with each transaction or queued mutation. */
|
|
22
23
|
const persistedWriteOptionsSchema = z
|
|
23
24
|
.object({
|
|
24
25
|
readAt: z.number().nullable().optional(),
|
|
26
|
+
reads: z.array(readDependencySchema).nullable().optional(),
|
|
25
27
|
idempotencyKey: z.string().optional(),
|
|
26
28
|
label: z.string().optional(),
|
|
27
29
|
// Aligned with the `WriteOptions` type: a claimed write persisted locally
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abloatai/humans",
|
|
3
|
-
"version": "0.59.
|
|
3
|
+
"version": "0.59.1",
|
|
4
4
|
"description": "The optional human-facing local-state package for Ablo: presence, live queries, and React bindings.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"directory": "packages/humans"
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
|
-
"@abloatai/transaction": "^0.59.
|
|
87
|
+
"@abloatai/transaction": "^0.59.1",
|
|
88
88
|
"mobx": "^6.13.7",
|
|
89
89
|
"uuid": "^11.1.0",
|
|
90
90
|
"zod": "^4.4.3"
|
|
@@ -242,7 +242,7 @@ import type { MutationOptions } from '@abloatai/transaction/client/resources/mut
|
|
|
242
242
|
*/
|
|
243
243
|
export type WriteOptions = Pick<
|
|
244
244
|
MutationOptions,
|
|
245
|
-
'readAt' | 'idempotencyKey' | 'label' | 'fenceToken' | 'claimRef'
|
|
245
|
+
'readAt' | 'reads' | 'idempotencyKey' | 'label' | 'fenceToken' | 'claimRef'
|
|
246
246
|
>;
|
|
247
247
|
|
|
248
248
|
/** A single mutation within a batch. Its `options` travel with it so the server
|
|
@@ -7,7 +7,7 @@ import type { DeltaConfirmationTracker } from './deltaConfirmation.js';
|
|
|
7
7
|
import type { MutationCommitResult } from '@abloatai/transaction/commit';
|
|
8
8
|
import type { DurableCommitEnvelope } from '@abloatai/transaction/commit';
|
|
9
9
|
import { AbloError, AbloNotFoundError } from '@abloatai/transaction/errors';
|
|
10
|
-
import { applyWriteOptions, normalizeModelKey, TX_TYPE_TO_MUTATION_OP, type WriteOperationFields } from './commitPayload.js';
|
|
10
|
+
import { applyWriteOptions, collectQueuedReads, normalizeModelKey, TX_TYPE_TO_MUTATION_OP, type WriteOperationFields } from './commitPayload.js';
|
|
11
11
|
import type { MutationOperationType } from '@abloatai/transaction/types';
|
|
12
12
|
|
|
13
13
|
export interface BatchProcessingContext {
|
|
@@ -138,6 +138,7 @@ export async function processBatch(ctx: BatchProcessingContext): Promise<void> {
|
|
|
138
138
|
origin: 'model_batch',
|
|
139
139
|
operations: batchOps.map(({ op }) => op),
|
|
140
140
|
sourceMutationIds: ctx.sourceMutationIdsFor(batch),
|
|
141
|
+
commitOptions: { reads: collectQueuedReads(batch) },
|
|
141
142
|
createdAt: Math.min(...batch.map((transaction) => transaction.createdAt)),
|
|
142
143
|
sealedAt: batch[0]?.commitEnvelope?.sealedAt ?? Date.now(),
|
|
143
144
|
sequence: batch[0]?.commitEnvelope?.sequence,
|
|
@@ -155,6 +156,9 @@ export async function processBatch(ctx: BatchProcessingContext): Promise<void> {
|
|
|
155
156
|
const result = ctx.parseMutationCommitResult(
|
|
156
157
|
await ctx.dispatchCommitBounded(operations, {
|
|
157
158
|
idempotencyKey: commitIdempotencyKey,
|
|
159
|
+
...(durableEnvelope.commitOptions.reads !== undefined
|
|
160
|
+
? { reads: durableEnvelope.commitOptions.reads }
|
|
161
|
+
: {}),
|
|
158
162
|
}),
|
|
159
163
|
);
|
|
160
164
|
await ctx.persistDurableCommitAcceptance(
|
|
@@ -160,6 +160,23 @@ export interface QueuedMutation {
|
|
|
160
160
|
confirmation?: Promise<void>;
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/** Merge per-write premises into the one batch-level read set sent on wire. */
|
|
164
|
+
export function collectQueuedReads(
|
|
165
|
+
transactions: readonly QueuedMutation[],
|
|
166
|
+
): MutationOptions['reads'] | undefined {
|
|
167
|
+
const declared = transactions
|
|
168
|
+
.map((transaction) => transaction.writeOptions?.reads)
|
|
169
|
+
.filter((reads) => reads !== undefined);
|
|
170
|
+
if (declared.length === 0) return undefined;
|
|
171
|
+
|
|
172
|
+
const unique = new Map<string, NonNullable<MutationOptions['reads']>[number]>();
|
|
173
|
+
for (const dependency of declared.flatMap((reads) => reads ?? [])) {
|
|
174
|
+
unique.set(JSON.stringify(dependency), dependency);
|
|
175
|
+
}
|
|
176
|
+
const reads = [...unique.values()];
|
|
177
|
+
return reads.length > 0 ? reads : null;
|
|
178
|
+
}
|
|
179
|
+
|
|
163
180
|
export const normalizeModelKey = (modelName: string): string =>
|
|
164
181
|
modelName.replace('Model', '').toLowerCase();
|
|
165
182
|
export const stripModelSuffix = (modelName: string): string => modelName.replace('Model', '');
|
|
@@ -4,7 +4,7 @@ import type { MutationStore } from './MutationStore.js';
|
|
|
4
4
|
import type { OptimisticUpdateEntry } from './localMutation.js';
|
|
5
5
|
import type { MutationCommitResult } from '@abloatai/transaction/commit';
|
|
6
6
|
import type { DurableCommitEnvelope } from '@abloatai/transaction/commit';
|
|
7
|
-
import { applyWriteOptions, TX_TYPE_TO_MUTATION_OP } from './commitPayload.js';
|
|
7
|
+
import { applyWriteOptions, collectQueuedReads, TX_TYPE_TO_MUTATION_OP } from './commitPayload.js';
|
|
8
8
|
|
|
9
9
|
export interface PendingDrainContext {
|
|
10
10
|
readonly runtime: RuntimeContext;
|
|
@@ -81,6 +81,7 @@ export async function drainPendingConfirmations(ctx: PendingDrainContext): Promi
|
|
|
81
81
|
origin: 'model_batch',
|
|
82
82
|
operations: projectedOperations,
|
|
83
83
|
sourceMutationIds: ctx.sourceMutationIdsFor(batch),
|
|
84
|
+
commitOptions: { reads: collectQueuedReads(batch) },
|
|
84
85
|
createdAt: Math.min(...batch.map((transaction) => transaction.createdAt)),
|
|
85
86
|
sealedAt: batch[0]?.commitEnvelope?.sealedAt ?? Date.now(),
|
|
86
87
|
sequence: batch[0]?.commitEnvelope?.sequence,
|
|
@@ -89,6 +90,9 @@ export async function drainPendingConfirmations(ctx: PendingDrainContext): Promi
|
|
|
89
90
|
const result = ctx.parseMutationCommitResult(
|
|
90
91
|
await ctx.dispatchCommitBounded(durableEnvelope.operations, {
|
|
91
92
|
idempotencyKey,
|
|
93
|
+
...(durableEnvelope.commitOptions.reads !== undefined
|
|
94
|
+
? { reads: durableEnvelope.commitOptions.reads }
|
|
95
|
+
: {}),
|
|
92
96
|
}),
|
|
93
97
|
);
|
|
94
98
|
await ctx.persistDurableCommitAcceptance(durableEnvelope, result);
|
|
@@ -24,11 +24,13 @@ import {
|
|
|
24
24
|
commitEnvelopeMemberSchema,
|
|
25
25
|
commitOutboxScopeSchema,
|
|
26
26
|
} from '@abloatai/transaction/commit';
|
|
27
|
+
import { readDependencySchema } from '@abloatai/transaction/coordination/schema';
|
|
27
28
|
|
|
28
29
|
/** The subset of a write's options that is stored with each transaction or queued mutation. */
|
|
29
30
|
const persistedWriteOptionsSchema = z
|
|
30
31
|
.object({
|
|
31
32
|
readAt: z.number().nullable().optional(),
|
|
33
|
+
reads: z.array(readDependencySchema).nullable().optional(),
|
|
32
34
|
idempotencyKey: z.string().optional(),
|
|
33
35
|
label: z.string().optional(),
|
|
34
36
|
// Aligned with the `WriteOptions` type: a claimed write persisted locally
|