experimental-a2 0.21.1 → 0.23.0
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/CHANGELOG.md +32 -0
- package/dist/{actor-HnGmrOZZ.d.ts → actor-CqibLgjQ.d.ts} +13 -5
- package/dist/actor-CqibLgjQ.d.ts.map +1 -0
- package/dist/actor-client.d.ts +1 -1
- package/dist/actor-react.d.ts +1 -1
- package/dist/actor-react.js +1 -1
- package/dist/actor-shared-DFET5q16.js.map +1 -1
- package/dist/actor.d.ts +1 -1
- package/dist/actor.js +3 -3
- package/dist/{ai-CvEnqVfo.d.ts → ai-DAu77CDr.d.ts} +2 -2
- package/dist/{ai-CvEnqVfo.d.ts.map → ai-DAu77CDr.d.ts.map} +1 -1
- package/dist/{ai-CzE672Q3.js → ai-EEfKyc-z.js} +2 -2
- package/dist/{ai-CzE672Q3.js.map → ai-EEfKyc-z.js.map} +1 -1
- package/dist/ai-client.d.ts +1 -1
- package/dist/ai-client.js +2 -2
- package/dist/ai-fx.d.ts +4 -2
- package/dist/ai-fx.d.ts.map +1 -1
- package/dist/ai-fx.js +234 -62
- package/dist/ai-fx.js.map +1 -1
- package/dist/{ai-harness-Bfk-Lycn.d.ts → ai-harness-DTjkAr2T.d.ts} +2 -2
- package/dist/{ai-harness-Bfk-Lycn.d.ts.map → ai-harness-DTjkAr2T.d.ts.map} +1 -1
- package/dist/ai-harness-dQeJaR1t.js.map +1 -1
- package/dist/ai-pi.d.ts +1 -1
- package/dist/ai-pi.js +1 -1
- package/dist/ai-react.d.ts +1 -1
- package/dist/ai-react.js +2 -2
- package/dist/ai-server.d.ts +3 -3
- package/dist/ai-server.d.ts.map +1 -1
- package/dist/ai-server.js +10 -4
- package/dist/ai-server.js.map +1 -1
- package/dist/ai.d.ts +1 -1
- package/dist/ai.js +1 -1
- package/dist/{client-9IeTsiAs.js → client-BQS4i_V9.js} +27 -4
- package/dist/client-BQS4i_V9.js.map +1 -0
- package/dist/client-CUMbS_38.d.ts.map +1 -1
- package/dist/client.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/{internal-Bc4PvfHk.js → internal-CvsrprnU.js} +32 -15
- package/dist/{internal-Bc4PvfHk.js.map → internal-CvsrprnU.js.map} +1 -1
- package/dist/react.js +1 -1
- package/dist/{runtime-consumer-BWmht60z.js → runtime-consumer-CIeL33wj.js} +2 -2
- package/dist/{runtime-consumer-BWmht60z.js.map → runtime-consumer-CIeL33wj.js.map} +1 -1
- package/dist/scheduler-qstash.d.ts +2 -2
- package/dist/scheduler-qstash.js +2 -2
- package/dist/scheduler-vercel.d.ts +2 -2
- package/dist/scheduler-vercel.js +2 -2
- package/dist/{server-DvIB8kj5.js → server-DtFrWxi3.js} +30 -8
- package/dist/server-DtFrWxi3.js.map +1 -0
- package/dist/{server-BshFZ0eL.d.ts → server-qgez0Xny.d.ts} +3 -3
- package/dist/{server-BshFZ0eL.d.ts.map → server-qgez0Xny.d.ts.map} +1 -1
- package/dist/server.d.ts +2 -2
- package/dist/server.js +1 -1
- package/dist/{session-clear-Bk_29eEw.d.ts → session-clear-D1PFe7xl.d.ts} +2 -2
- package/dist/session-clear-D1PFe7xl.d.ts.map +1 -0
- package/dist/{store-Z2qjw50n.d.ts → store-D0MhBvg6.d.ts} +15 -6
- package/dist/store-D0MhBvg6.d.ts.map +1 -0
- package/dist/store-N8PXxDAS.js.map +1 -1
- package/dist/store-memory.d.ts +1 -1
- package/dist/store-memory.d.ts.map +1 -1
- package/dist/store-memory.js +15 -17
- package/dist/store-memory.js.map +1 -1
- package/dist/{store-polling-DYNyxCev.js → store-polling-BZt2Rf3D.js} +2 -2
- package/dist/{store-polling-DYNyxCev.js.map → store-polling-BZt2Rf3D.js.map} +1 -1
- package/dist/store-postgres.d.ts +1 -1
- package/dist/store-postgres.d.ts.map +1 -1
- package/dist/store-postgres.js +31 -40
- package/dist/store-postgres.js.map +1 -1
- package/dist/store-presence-polling-JuAy2Ck2.js +111 -0
- package/dist/store-presence-polling-JuAy2Ck2.js.map +1 -0
- package/dist/store-redis-http.d.ts +1 -1
- package/dist/store-redis-http.js +3 -3
- package/dist/{store-redis-notify-IiVdlYHc.js → store-redis-notify-CBDVJxA_.js} +545 -399
- package/dist/store-redis-notify-CBDVJxA_.js.map +1 -0
- package/dist/store-redis.d.ts +1 -1
- package/dist/store-redis.d.ts.map +1 -1
- package/dist/store-redis.js +3 -4
- package/dist/store-redis.js.map +1 -1
- package/dist/store-sqlite.d.ts +1 -1
- package/dist/store-sqlite.d.ts.map +1 -1
- package/dist/store-sqlite.js +16 -18
- package/dist/store-sqlite.js.map +1 -1
- package/dist/store-valkey.d.ts +33 -0
- package/dist/store-valkey.d.ts.map +1 -0
- package/dist/store-valkey.js +218 -0
- package/dist/store-valkey.js.map +1 -0
- package/dist/stream-buffer-DrTyiIuo.js +57 -0
- package/dist/stream-buffer-DrTyiIuo.js.map +1 -0
- package/dist/{wire-CZGqNZRx.js → wire-CV-Z7wz-.js} +3 -3
- package/dist/{wire-CZGqNZRx.js.map → wire-CV-Z7wz-.js.map} +1 -1
- package/docs/actors/01-introduction.mdx +5 -2
- package/docs/agents/03-conversations.mdx +9 -8
- package/docs/agents/04-harnesses.mdx +7 -4
- package/docs/client/03-presence.mdx +1 -0
- package/docs/reference/01-core.mdx +4 -2
- package/docs/reference/02-server.mdx +5 -1
- package/docs/reference/04-ai.mdx +13 -4
- package/docs/reference/06-errors.mdx +1 -1
- package/docs/server/01-consumers.mdx +4 -1
- package/docs/server/06-guarantees.mdx +7 -4
- package/docs/stores/01-overview.mdx +1 -0
- package/docs/stores/02-redis.mdx +18 -10
- package/docs/stores/03-redis-http.mdx +6 -4
- package/docs/stores/04-postgres.mdx +2 -2
- package/docs/stores/07-valkey.mdx +97 -0
- package/docs/upgrades/01-overview.mdx +1 -0
- package/docs/upgrades/78-0-22.mdx +115 -0
- package/examples/playground/app/a0/sandbox.ts +58 -13
- package/examples/playground/app/a2slack/db/a2.ts +7 -4
- package/examples/playground/app/a2slack/db/migrations/0004_a2_session_event_ids.sql +2 -0
- package/examples/playground/app/a2slack/db/migrations/meta/0004_snapshot.json +1165 -0
- package/examples/playground/app/a2slack/db/migrations/meta/_journal.json +8 -1
- package/examples/playground/app/chat/server.ts +2 -2
- package/examples/playground/lib/store.ts +1 -1
- package/examples/playground/package.json +1 -1
- package/package.json +8 -2
- package/sql/postgres/drizzle-schema.ts +7 -4
- package/sql/postgres/schema.sql +24 -3
- package/src/actor-shared.ts +10 -2
- package/src/ai-agent.ts +13 -0
- package/src/ai-fx.ts +365 -78
- package/src/ai-harness.ts +7 -0
- package/src/client.ts +19 -2
- package/src/deterministic-id.ts +1 -1
- package/src/internal.ts +42 -11
- package/src/libfx.d.ts +2 -0
- package/src/presence-recovery.ts +18 -11
- package/src/presence-tombstones.ts +52 -0
- package/src/push-envelope.ts +2 -2
- package/src/server.ts +49 -15
- package/src/store-memory.ts +24 -32
- package/src/store-postgres.ts +43 -53
- package/src/store-presence-polling.ts +59 -34
- package/src/store-redis-core.ts +595 -439
- package/src/store-redis-notify.ts +82 -28
- package/src/store-redis.ts +2 -3
- package/src/store-sqlite.ts +34 -40
- package/src/store-valkey.ts +376 -0
- package/src/store.ts +14 -5
- package/dist/actor-HnGmrOZZ.d.ts.map +0 -1
- package/dist/client-9IeTsiAs.js.map +0 -1
- package/dist/server-DvIB8kj5.js.map +0 -1
- package/dist/session-clear-Bk_29eEw.d.ts.map +0 -1
- package/dist/store-Z2qjw50n.d.ts.map +0 -1
- package/dist/store-presence-polling-CzQ0wdtM.js +0 -94
- package/dist/store-presence-polling-CzQ0wdtM.js.map +0 -1
- package/dist/store-redis-notify-IiVdlYHc.js.map +0 -1
- package/dist/stream-buffer-Cx7AAPGq.js +0 -23
- package/dist/stream-buffer-Cx7AAPGq.js.map +0 -1
package/src/store-postgres.ts
CHANGED
|
@@ -570,9 +570,9 @@ const INSERTED_AFTER = `(
|
|
|
570
570
|
|
|
571
571
|
/**
|
|
572
572
|
* An append's checks, under the session lock: the session's head and pending
|
|
573
|
-
* state, and what its supplied `ids` say.
|
|
574
|
-
* this session already, the batch is a retry: it
|
|
575
|
-
* rows. Some owned
|
|
573
|
+
* state, and what its supplied `ids` say. Ids are unique per session. When
|
|
574
|
+
* every event's id belongs to this session already, the batch is a retry: it
|
|
575
|
+
* stops with the committed rows. Some owned aborts, as does scheduled work
|
|
576
576
|
* from at or below the clear marker (`origin`). Rows below the marker own no
|
|
577
577
|
* id.
|
|
578
578
|
*/
|
|
@@ -586,15 +586,11 @@ const APPEND_CHECKS = `
|
|
|
586
586
|
select event.session_id, event.idx, event.event_type, event.payload,
|
|
587
587
|
event.event_id, event.created_at, event.cause
|
|
588
588
|
from a2_events event, boundary
|
|
589
|
-
where event.
|
|
590
|
-
and
|
|
591
|
-
|
|
589
|
+
where event.session_id = ${SESSION}
|
|
590
|
+
and event.event_id = any(${param.texts('ids')})
|
|
591
|
+
and event.idx >= boundary.clear_index
|
|
592
592
|
)
|
|
593
593
|
select case
|
|
594
|
-
when exists (select 1 from owned where session_id <> ${SESSION}) then
|
|
595
|
-
jsonb_build_object('abort', 'foreign', 'event_id', (
|
|
596
|
-
select event_id from owned where session_id <> ${SESSION}
|
|
597
|
-
order by idx limit 1))
|
|
598
594
|
when (select count(*) from owned) = ${param.bigint('count')} then
|
|
599
595
|
jsonb_build_object('stop', 'replay',
|
|
600
596
|
'has_pending', boundary.has_pending,
|
|
@@ -1083,9 +1079,9 @@ const RETURN_CHECKS = `
|
|
|
1083
1079
|
jsonb_build_object('stop', 'superseded')
|
|
1084
1080
|
when exists (
|
|
1085
1081
|
select 1 from a2_events event
|
|
1086
|
-
where event.
|
|
1087
|
-
and
|
|
1088
|
-
|
|
1082
|
+
where event.session_id = ${SESSION}
|
|
1083
|
+
and event.event_id = any(${param.texts('ids')})
|
|
1084
|
+
and event.idx >= ${clearIndexOf(SESSION)}
|
|
1089
1085
|
) then jsonb_build_object('abort', 'owned')
|
|
1090
1086
|
else jsonb_build_object('base', (
|
|
1091
1087
|
select coalesce(max(idx), 0) from a2_events
|
|
@@ -1737,8 +1733,8 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
1737
1733
|
if (events.length === 0) return { outcome: 'completed', events: [] }
|
|
1738
1734
|
const { rows: existing } = await query(
|
|
1739
1735
|
`select session_id, idx, event_type, payload, event_id, created_at, cause
|
|
1740
|
-
from a2_events where event_id = any($
|
|
1741
|
-
[ids],
|
|
1736
|
+
from a2_events where session_id = $1 and event_id = any($2::text[])`,
|
|
1737
|
+
[sessionId, ids],
|
|
1742
1738
|
)
|
|
1743
1739
|
const byId = new Map(
|
|
1744
1740
|
existing.map((row) => [row['event_id'] as string, row]),
|
|
@@ -1754,7 +1750,6 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
1754
1750
|
const cause = row ? decodeCause(row['cause']) : null
|
|
1755
1751
|
return (
|
|
1756
1752
|
!row ||
|
|
1757
|
-
row['session_id'] !== sessionId ||
|
|
1758
1753
|
cause?.consumer !== consumer ||
|
|
1759
1754
|
cause.lane !== lane ||
|
|
1760
1755
|
cause.index !== index ||
|
|
@@ -1849,11 +1844,6 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
1849
1844
|
},
|
|
1850
1845
|
abort: (reason) => {
|
|
1851
1846
|
switch (reason['abort']) {
|
|
1852
|
-
case 'foreign':
|
|
1853
|
-
return new A2Error(
|
|
1854
|
-
'PARTIAL_DUPLICATE_BATCH',
|
|
1855
|
-
`event id '${String(reason['event_id'])}' already exists in another session`,
|
|
1856
|
-
)
|
|
1857
1847
|
case 'mixed': {
|
|
1858
1848
|
const owned = asNumber(reason['owned'])
|
|
1859
1849
|
return new A2Error(
|
|
@@ -2710,40 +2700,38 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
2710
2700
|
const atMs = meta.at.getTime()
|
|
2711
2701
|
// Expiry anchors on the storage clock — the sender's `at`
|
|
2712
2702
|
// orders writes but never anchors their lifetime.
|
|
2713
|
-
const
|
|
2703
|
+
const nowMs = clock.now().getTime()
|
|
2704
|
+
const expiresAtMs = nowMs + meta.ttlMs
|
|
2714
2705
|
const applied: Record<string, unknown> = nullProtoRecord()
|
|
2715
2706
|
const write = async (query: Query): Promise<void> => {
|
|
2716
2707
|
for (const [field, value] of entries) {
|
|
2717
|
-
// Field-wise LWW
|
|
2718
|
-
//
|
|
2708
|
+
// Field-wise LWW against values and tombstones (a JSON null
|
|
2709
|
+
// value, a delete's `at` kept until it expires) alike: an
|
|
2710
|
+
// unexpired stored row with a strictly newer `at` survives;
|
|
2711
|
+
// ties go to the incoming write (memory parity).
|
|
2719
2712
|
// oxlint-disable-next-line no-await-in-loop -- independent per-field registers
|
|
2720
|
-
const result = await (
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
meta.seen,
|
|
2743
|
-
atMs,
|
|
2744
|
-
expiresAtMs,
|
|
2745
|
-
],
|
|
2746
|
-
))
|
|
2713
|
+
const result = await query(
|
|
2714
|
+
`insert into a2_presence
|
|
2715
|
+
(ns, participant, field, value, seen, at, expires_at)
|
|
2716
|
+
values ($1, $2, $3, $4::jsonb, $5, $6, $7)
|
|
2717
|
+
on conflict (ns, participant, field) do update set
|
|
2718
|
+
value = excluded.value,
|
|
2719
|
+
seen = excluded.seen,
|
|
2720
|
+
at = excluded.at,
|
|
2721
|
+
expires_at = excluded.expires_at
|
|
2722
|
+
where a2_presence.at <= excluded.at
|
|
2723
|
+
or a2_presence.expires_at <= $8${notifications ? ' returning field' : ''}`,
|
|
2724
|
+
[
|
|
2725
|
+
ns,
|
|
2726
|
+
participant,
|
|
2727
|
+
field,
|
|
2728
|
+
jsonb(value),
|
|
2729
|
+
meta.seen,
|
|
2730
|
+
atMs,
|
|
2731
|
+
expiresAtMs,
|
|
2732
|
+
nowMs,
|
|
2733
|
+
],
|
|
2734
|
+
)
|
|
2747
2735
|
if (result.rows.length) applied[field] = structuredClone(value)
|
|
2748
2736
|
}
|
|
2749
2737
|
}
|
|
@@ -2781,7 +2769,7 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
2781
2769
|
)
|
|
2782
2770
|
select participant, field, value, seen, at, expires_at
|
|
2783
2771
|
from a2_presence
|
|
2784
|
-
where ns = $1 and expires_at > $2
|
|
2772
|
+
where ns = $1 and expires_at > $2 and value <> 'null'::jsonb
|
|
2785
2773
|
order by participant, field`,
|
|
2786
2774
|
[ns, now],
|
|
2787
2775
|
)
|
|
@@ -3025,7 +3013,9 @@ export function postgres(options: PostgresStoreOptions = {}): PostgresStore {
|
|
|
3025
3013
|
const subscription = presenceSubscriptions.get(ns)
|
|
3026
3014
|
await subscription?.ready
|
|
3027
3015
|
await subscription?.poll.ready
|
|
3028
|
-
return
|
|
3016
|
+
return subscription
|
|
3017
|
+
? subscription.poll.snapshot(() => read(ns))
|
|
3018
|
+
: read(ns)
|
|
3029
3019
|
}
|
|
3030
3020
|
presence.subscribe = (ns, onPatch) => {
|
|
3031
3021
|
if (closed) throw new A2Error('STORE_UNAVAILABLE', 'the store was closed')
|
|
@@ -19,6 +19,11 @@ export function pollPresence({
|
|
|
19
19
|
sleep?: (ms: number) => Sleeper
|
|
20
20
|
}): {
|
|
21
21
|
ready: Promise<void>
|
|
22
|
+
/**
|
|
23
|
+
* Runs a foreground snapshot read and counts it as a poll, so a stamp a
|
|
24
|
+
* viewer paints from it is one later deletes carry.
|
|
25
|
+
*/
|
|
26
|
+
snapshot(read: () => Promise<PresenceRow[]>): Promise<PresenceRow[]>
|
|
22
27
|
update(patch: PresencePatch): void
|
|
23
28
|
wake(): void
|
|
24
29
|
stop(): void
|
|
@@ -45,6 +50,46 @@ export function pollPresence({
|
|
|
45
50
|
// Seeding is best effort; foreground snapshots still report read errors.
|
|
46
51
|
.catch(() => undefined)
|
|
47
52
|
|
|
53
|
+
/** Delivers how `rows` differ from what was seen; true when they do. */
|
|
54
|
+
const reconcile = (rows: PresenceRow[]): boolean => {
|
|
55
|
+
const live = new Set<string>()
|
|
56
|
+
let changed = false
|
|
57
|
+
for (const row of rows) {
|
|
58
|
+
const id = key(row)
|
|
59
|
+
live.add(id)
|
|
60
|
+
const prior = seen.get(id)
|
|
61
|
+
seen.set(id, row)
|
|
62
|
+
if (
|
|
63
|
+
prior?.seen === row.seen &&
|
|
64
|
+
prior.at.getTime() === row.at.getTime() &&
|
|
65
|
+
JSON.stringify(prior.value) === JSON.stringify(row.value)
|
|
66
|
+
)
|
|
67
|
+
continue
|
|
68
|
+
changed = true
|
|
69
|
+
deliver({
|
|
70
|
+
participant: row.participant,
|
|
71
|
+
values: { [row.field]: row.value },
|
|
72
|
+
seen: row.seen,
|
|
73
|
+
at: row.at,
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
for (const [id, row] of seen) {
|
|
77
|
+
if (live.has(id)) continue
|
|
78
|
+
seen.delete(id)
|
|
79
|
+
changed = true
|
|
80
|
+
deliver({
|
|
81
|
+
participant: row.participant,
|
|
82
|
+
values: { [row.field]: null },
|
|
83
|
+
seen: row.seen,
|
|
84
|
+
// The last seen stamp, never this poller's clock: only
|
|
85
|
+
// senders' clocks order the plane, and an observer-stamped
|
|
86
|
+
// delete could outrank the sender's next set.
|
|
87
|
+
at: row.at,
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
return changed
|
|
91
|
+
}
|
|
92
|
+
|
|
48
93
|
const run = async (): Promise<void> => {
|
|
49
94
|
await ready
|
|
50
95
|
for (;;) {
|
|
@@ -60,39 +105,7 @@ export function pollPresence({
|
|
|
60
105
|
const rows = await read()
|
|
61
106
|
if (stopped) return
|
|
62
107
|
if (version !== reading) continue
|
|
63
|
-
|
|
64
|
-
let changed = false
|
|
65
|
-
for (const row of rows) {
|
|
66
|
-
const id = key(row)
|
|
67
|
-
live.add(id)
|
|
68
|
-
const prior = seen.get(id)
|
|
69
|
-
seen.set(id, row)
|
|
70
|
-
if (
|
|
71
|
-
prior?.seen === row.seen &&
|
|
72
|
-
prior.at.getTime() === row.at.getTime() &&
|
|
73
|
-
JSON.stringify(prior.value) === JSON.stringify(row.value)
|
|
74
|
-
)
|
|
75
|
-
continue
|
|
76
|
-
changed = true
|
|
77
|
-
deliver({
|
|
78
|
-
participant: row.participant,
|
|
79
|
-
values: { [row.field]: row.value },
|
|
80
|
-
seen: row.seen,
|
|
81
|
-
at: row.at,
|
|
82
|
-
})
|
|
83
|
-
}
|
|
84
|
-
for (const [id, row] of seen) {
|
|
85
|
-
if (live.has(id)) continue
|
|
86
|
-
seen.delete(id)
|
|
87
|
-
changed = true
|
|
88
|
-
deliver({
|
|
89
|
-
participant: row.participant,
|
|
90
|
-
values: { [row.field]: null },
|
|
91
|
-
seen: row.seen,
|
|
92
|
-
at: new Date(),
|
|
93
|
-
})
|
|
94
|
-
}
|
|
95
|
-
if (changed) activate()
|
|
108
|
+
if (reconcile(rows)) activate()
|
|
96
109
|
else if (version === before)
|
|
97
110
|
interval = Math.min(interval * 2, POLL_TIMINGS.idleCeilingMs)
|
|
98
111
|
} catch {
|
|
@@ -103,10 +116,22 @@ export function pollPresence({
|
|
|
103
116
|
void run()
|
|
104
117
|
return {
|
|
105
118
|
ready,
|
|
119
|
+
async snapshot(readSnapshot) {
|
|
120
|
+
const reading = version
|
|
121
|
+
const rows = await readSnapshot()
|
|
122
|
+
if (!stopped && version === reading && reconcile(rows)) activate()
|
|
123
|
+
return rows
|
|
124
|
+
},
|
|
106
125
|
wake: activate,
|
|
107
126
|
update(patch) {
|
|
108
127
|
for (const [field, value] of Object.entries(patch.values)) {
|
|
109
|
-
|
|
128
|
+
const id = key({ participant: patch.participant, field })
|
|
129
|
+
// A deleted field is one the next read should not find.
|
|
130
|
+
if (value === null) {
|
|
131
|
+
seen.delete(id)
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
seen.set(id, {
|
|
110
135
|
participant: patch.participant,
|
|
111
136
|
field,
|
|
112
137
|
value,
|