@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.
@@ -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.0",
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.0",
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