@astrosheep/square 0.3.23 → 0.3.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,13 @@
1
1
  import { type InboxMembership } from './model.js';
2
+ export interface PresentedAttention {
3
+ ownerId: string;
4
+ squarePath: string;
5
+ name: string;
6
+ actIndex: number;
7
+ }
2
8
  export declare function presentedPath(env?: NodeJS.ProcessEnv): string;
9
+ /** Read the current presentation facts once for a derived evidence projection. */
10
+ export declare function readPresentedAttentions(env?: NodeJS.ProcessEnv, now?: number): PresentedAttention[];
3
11
  export declare function hasPresentedForOwner(ownerId: string, squarePath: string, name: string, actIndex: number, env?: NodeJS.ProcessEnv, now?: number): boolean;
4
12
  /** True when any current participant owner has already received this attention. */
5
13
  export declare function hasPresentedAttention(squarePath: string, name: string, actIndex: number, env?: NodeJS.ProcessEnv, now?: number): boolean;
package/dist/presented.js CHANGED
@@ -48,6 +48,15 @@ function readRows(filePath, now = Date.now()) {
48
48
  }
49
49
  return rows;
50
50
  }
51
+ /** Read the current presentation facts once for a derived evidence projection. */
52
+ export function readPresentedAttentions(env = process.env, now = Date.now()) {
53
+ return readRows(presentedPath(env), now).map((row) => ({
54
+ ownerId: row.owner_id,
55
+ squarePath: canonicalSquarePath(row.square_path),
56
+ name: row.name,
57
+ actIndex: row.act_index,
58
+ }));
59
+ }
51
60
  function writeRows(filePath, rows) {
52
61
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
53
62
  const temp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
@@ -28,6 +28,7 @@ export declare function registryPath(): string;
28
28
  export declare function canonicalSquarePath(squarePath: string): string;
29
29
  export declare function recordJoin(sessionId: string, name: string, squarePath: string, options?: RegistryWriteOptions): void;
30
30
  export declare function recordDone(sessionId: string, name: string, squarePath: string, options?: RegistryWriteOptions): void;
31
+ export declare function readActiveBindings(now?: number): RegistryBinding[];
31
32
  export declare function lookupSessionBindings(sessionId: string, now?: number): RegistryBinding[];
32
33
  export declare function lookupSession(sessionId: string, now?: number): Array<{
33
34
  name: string;
package/dist/registry.js CHANGED
@@ -205,7 +205,7 @@ export function recordJoin(sessionId, name, squarePath, options = {}) {
205
205
  export function recordDone(sessionId, name, squarePath, options = {}) {
206
206
  writeLifecycle('done', sessionId, name, squarePath, options);
207
207
  }
208
- function readActiveBindings(now = Date.now()) {
208
+ export function readActiveBindings(now = Date.now()) {
209
209
  try {
210
210
  return foldRegistry(fs.readFileSync(registryPath(), 'utf8'), now);
211
211
  }
package/dist/runtime.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type ActivityId, type Reach } from './square-core.js';
1
+ import { type ActivityId, type LandedAudienceReplay, type Reach } from './square-core.js';
2
2
  import { type StoredAct, type SquareState, type HoldState, type ActivityObservation, type ObservationState, type WatchLease } from './model.js';
3
3
  export declare const WATCH_HEARTBEAT_MS: number;
4
4
  export declare const SLEEP_MS: number;
@@ -36,7 +36,7 @@ export declare function publicActs(acts: StoredAct[]): Array<Extract<StoredAct,
36
36
  }>>;
37
37
  export declare function observationFor(squareState: SquareState, name: string, index: number): ActivityObservation | undefined;
38
38
  export declare function recordObservation(squareState: SquareState, name: string, index: number, state: ObservationState, at?: number, ownerId?: string): boolean;
39
- export declare function readCursor(squareState: SquareState, name: string): number;
39
+ export declare function readCursor(squareState: SquareState, name: string, landed?: LandedAudienceReplay): number;
40
40
  export declare function latestActIndex(acts: StoredAct[]): number;
41
41
  export declare function freshWatchLease(squareState: SquareState, name: string, at?: number): WatchLease | undefined;
42
42
  export declare function watchLease(squareState: SquareState, name: string): WatchLease | undefined;
package/dist/runtime.js CHANGED
@@ -1,4 +1,4 @@
1
- import { audienceIncludes, audienceOf, fold, formatActivityId, landedAudienceIncludes } from './square-core.js';
1
+ import { audienceIncludes, audienceOf, fold, formatActivityId, replayLandedAudiences } from './square-core.js';
2
2
  import { SquareError, findParticipantName, nameKey, sameName, } from './model.js';
3
3
  function parseIntegerEnvValue(name, raw, fallback) {
4
4
  if (raw === undefined)
@@ -162,21 +162,22 @@ export function recordObservation(squareState, name, index, state, at = Date.now
162
162
  ((squareState.runtime.observations ??= {})[key] ??= {})[id] = next;
163
163
  return true;
164
164
  }
165
- export function readCursor(squareState, name) {
166
- const boundary = lastJoinIndex(squareState.acts, name) ?? -1;
165
+ export function readCursor(squareState, name, landed = replayLandedAudiences(squareState.acts)) {
166
+ const recipient = landed.resolveParticipant(name) ?? name;
167
+ const boundary = landed.lastJoinIndex(recipient) ?? -1;
167
168
  let cursor = boundary;
168
169
  for (const act of squareState.acts) {
169
170
  if (act.index <= boundary || act.kind === 'read' || act.actor === undefined)
170
171
  continue;
171
- if (sameName(act.actor, name)) {
172
+ if (sameName(act.actor, recipient)) {
172
173
  cursor = act.index;
173
174
  continue;
174
175
  }
175
- if (!landedAudienceIncludes(squareState.acts, act, name)) {
176
+ if (!landed.includes(act, recipient)) {
176
177
  cursor = act.index;
177
178
  continue;
178
179
  }
179
- if (observationFor(squareState, name, act.index)?.state !== 'seen')
180
+ if (squareState.runtime.observations?.[recipient]?.[formatActivityId(act.index)]?.state !== 'seen')
180
181
  break;
181
182
  cursor = act.index;
182
183
  }
@@ -82,6 +82,15 @@ export interface FoldedSquareState {
82
82
  /** Derived sender blocks; kept outside the artifact schema. */
83
83
  ignored: Map<string, Participant[]>;
84
84
  }
85
+ export interface LandedAudienceReplay {
86
+ readonly participants: readonly Participant[];
87
+ readonly joined: readonly Participant[];
88
+ readonly replayedActivityCount: number;
89
+ recipientsFor(activity: Act): readonly Participant[];
90
+ includes(activity: Act, participant: Participant): boolean;
91
+ lastJoinIndex(participant: Participant): number | undefined;
92
+ resolveParticipant(name: Participant): Participant | undefined;
93
+ }
85
94
  export type ValidationResult = {
86
95
  ok: true;
87
96
  } | {
@@ -120,14 +129,11 @@ export declare function audienceIncludes(audience: Audience, name: string): bool
120
129
  export declare function resolveAudience(audience: Audience, candidateNames: readonly string[]): string[];
121
130
  export declare function activeListeners(state: FoldedSquareState, sender: string): string[];
122
131
  export declare function isIgnored(state: FoldedSquareState, listener: string, sender: string): boolean;
123
- export declare function audienceBefore(acts: readonly Act[], say: Extract<Act, {
124
- kind: 'say';
125
- }>): string[];
126
- /** Whether a peer say was directed to this participant when it landed. */
127
- export declare function landedAudienceIncludes(acts: readonly Act[], activity: Act, viewer: string): boolean;
128
132
  export declare function listeningTo(state: FoldedSquareState, listener: string): string[];
129
133
  export declare function isListening(state: FoldedSquareState, listener: string, sender: string): boolean;
130
134
  export declare function fold(acts: readonly Act[]): FoldedSquareState;
135
+ /** Replay the activity stream once to fix every say's audience at landing. */
136
+ export declare function replayLandedAudiences(acts: readonly Act[]): LandedAudienceReplay;
131
137
  export declare function validate(state: FoldedSquareState, act: Act, options?: SquareValidationOptions): ValidationResult;
132
138
  export declare function perceive(act: Act, viewer: Participant | string): Perception;
133
139
  export {};
@@ -67,11 +67,9 @@ export function activeListeners(state, sender) {
67
67
  export function isIgnored(state, listener, sender) {
68
68
  return (state.ignored.get(nameKey(listener)) ?? []).some((target) => sameName(target, sender));
69
69
  }
70
- export function audienceBefore(acts, say) {
71
- const position = acts.findIndex((act) => act === say || ('index' in act && 'index' in say && act.index === say.index));
72
- const before = fold(position < 0 ? acts : acts.slice(0, position));
70
+ function recipientsAtLanding(before, say) {
73
71
  const audience = audienceOf(say);
74
- const mentionTargets = resolveAudience(audience, before.joined);
72
+ const mentionTargets = resolveAudience(audience, before.participants.filter((participant) => participant.joined).map((participant) => participant.name));
75
73
  const listeners = activeListeners(before, say.actor);
76
74
  const recipients = [];
77
75
  for (const name of [...mentionTargets, ...listeners]) {
@@ -83,12 +81,6 @@ export function audienceBefore(acts, say) {
83
81
  }
84
82
  return recipients;
85
83
  }
86
- /** Whether a peer say was directed to this participant when it landed. */
87
- export function landedAudienceIncludes(acts, activity, viewer) {
88
- if (activity.kind !== 'say' || sameName(activity.actor, viewer))
89
- return false;
90
- return audienceBefore(acts, activity).some((recipient) => sameName(recipient, viewer));
91
- }
92
84
  export function listeningTo(state, listener) {
93
85
  return [...(state.listening.get(nameKey(listener)) ?? [])];
94
86
  }
@@ -142,97 +134,154 @@ function bellRecentAt(state, actor, at, windowMs) {
142
134
  }
143
135
  return latest;
144
136
  }
145
- export function fold(acts) {
137
+ function createFoldAccumulator() {
146
138
  const ordered = [];
147
139
  const byKey = new Map();
148
140
  const hold = { active: false };
149
- const state = {
150
- participants: ordered,
151
- hold,
152
- joined: [],
153
- done: [],
154
- throttleActivityAts: [],
155
- bellSayAtsByActor: new Map(),
156
- listening: new Map(),
157
- ignored: new Map(),
141
+ return {
142
+ byKey,
143
+ ordered,
144
+ state: {
145
+ participants: ordered,
146
+ hold,
147
+ joined: [],
148
+ done: [],
149
+ throttleActivityAts: [],
150
+ bellSayAtsByActor: new Map(),
151
+ listening: new Map(),
152
+ ignored: new Map(),
153
+ },
158
154
  };
159
- for (const act of acts) {
160
- const actor = actorOf(act);
161
- const snapshot = actor === undefined ? undefined : touchParticipant(byKey, ordered, actor);
162
- switch (act.kind) {
163
- case 'join':
164
- if (snapshot !== undefined) {
165
- snapshot.joined = true;
166
- snapshot.done = false;
167
- snapshot.lastActiveAt = act.at ?? snapshot.lastActiveAt;
168
- }
169
- break;
170
- case 'done':
171
- if (snapshot !== undefined) {
172
- snapshot.joined = false;
173
- snapshot.done = true;
174
- snapshot.lastActiveAt = act.at ?? snapshot.lastActiveAt;
175
- state.listening.delete(nameKey(snapshot.name));
176
- }
177
- break;
178
- case 'listen': {
179
- const key = nameKey(act.actor);
180
- const targets = state.listening.get(key) ?? [];
181
- if (!targets.some((target) => sameName(target, act.target)))
182
- targets.push(act.target);
183
- state.listening.set(key, targets);
184
- const ignored = (state.ignored.get(key) ?? []).filter((target) => !sameName(target, act.target));
185
- if (ignored.length === 0)
186
- state.ignored.delete(key);
187
- else
188
- state.ignored.set(key, ignored);
189
- break;
155
+ }
156
+ function applyActivity(accumulator, act) {
157
+ const { state, byKey, ordered } = accumulator;
158
+ const actor = actorOf(act);
159
+ const snapshot = actor === undefined ? undefined : touchParticipant(byKey, ordered, actor);
160
+ switch (act.kind) {
161
+ case 'join':
162
+ if (snapshot !== undefined) {
163
+ snapshot.joined = true;
164
+ snapshot.done = false;
165
+ snapshot.lastActiveAt = act.at ?? snapshot.lastActiveAt;
190
166
  }
191
- case 'ignore': {
192
- const key = nameKey(act.actor);
193
- const targets = (state.listening.get(key) ?? []).filter((target) => !sameName(target, act.target));
194
- if (targets.length === 0)
195
- state.listening.delete(key);
196
- else
197
- state.listening.set(key, targets);
198
- const ignored = state.ignored.get(key) ?? [];
199
- if (!ignored.some((target) => sameName(target, act.target)))
200
- ignored.push(act.target);
201
- state.ignored.set(key, ignored);
202
- break;
167
+ break;
168
+ case 'done':
169
+ if (snapshot !== undefined) {
170
+ snapshot.joined = false;
171
+ snapshot.done = true;
172
+ snapshot.lastActiveAt = act.at ?? snapshot.lastActiveAt;
173
+ state.listening.delete(nameKey(snapshot.name));
203
174
  }
204
- case 'say':
205
- if (snapshot !== undefined) {
206
- snapshot.activityCount += 1;
207
- snapshot.lastActiveAt = act.at ?? snapshot.lastActiveAt;
208
- }
209
- pushThrottleAt(state, act.at);
210
- if (act.reach === 'bell')
211
- pushBellAt(state, act.actor, act.at);
212
- break;
213
- case 'hold':
214
- hold.active = true;
215
- hold.at = act.at;
216
- hold.reason = act.body;
217
- hold.actor = act.actor;
218
- break;
219
- case 'resume':
220
- hold.active = false;
221
- delete hold.at;
222
- delete hold.reason;
223
- delete hold.actor;
224
- break;
225
- case 'read':
226
- if (snapshot !== undefined) {
227
- snapshot.lastReadThrough = Math.max(snapshot.lastReadThrough, act.through);
228
- }
229
- break;
175
+ break;
176
+ case 'listen': {
177
+ const key = nameKey(act.actor);
178
+ const targets = state.listening.get(key) ?? [];
179
+ if (!targets.some((target) => sameName(target, act.target)))
180
+ targets.push(act.target);
181
+ state.listening.set(key, targets);
182
+ const ignored = (state.ignored.get(key) ?? []).filter((target) => !sameName(target, act.target));
183
+ if (ignored.length === 0)
184
+ state.ignored.delete(key);
185
+ else
186
+ state.ignored.set(key, ignored);
187
+ break;
188
+ }
189
+ case 'ignore': {
190
+ const key = nameKey(act.actor);
191
+ const targets = (state.listening.get(key) ?? []).filter((target) => !sameName(target, act.target));
192
+ if (targets.length === 0)
193
+ state.listening.delete(key);
194
+ else
195
+ state.listening.set(key, targets);
196
+ const ignored = state.ignored.get(key) ?? [];
197
+ if (!ignored.some((target) => sameName(target, act.target)))
198
+ ignored.push(act.target);
199
+ state.ignored.set(key, ignored);
200
+ break;
230
201
  }
202
+ case 'say':
203
+ if (snapshot !== undefined) {
204
+ snapshot.activityCount += 1;
205
+ snapshot.lastActiveAt = act.at ?? snapshot.lastActiveAt;
206
+ }
207
+ pushThrottleAt(state, act.at);
208
+ if (act.reach === 'bell')
209
+ pushBellAt(state, act.actor, act.at);
210
+ break;
211
+ case 'hold':
212
+ state.hold.active = true;
213
+ state.hold.at = act.at;
214
+ state.hold.reason = act.body;
215
+ state.hold.actor = act.actor;
216
+ break;
217
+ case 'resume':
218
+ state.hold.active = false;
219
+ delete state.hold.at;
220
+ delete state.hold.reason;
221
+ delete state.hold.actor;
222
+ break;
223
+ case 'read':
224
+ if (snapshot !== undefined) {
225
+ snapshot.lastReadThrough = Math.max(snapshot.lastReadThrough, act.through);
226
+ }
227
+ break;
231
228
  }
229
+ }
230
+ function finishFold(accumulator) {
231
+ const { state, ordered } = accumulator;
232
232
  state.joined = ordered.filter((item) => item.joined).map((item) => item.name);
233
233
  state.done = ordered.filter((item) => item.done).map((item) => item.name);
234
234
  return state;
235
235
  }
236
+ export function fold(acts) {
237
+ const accumulator = createFoldAccumulator();
238
+ for (const act of acts)
239
+ applyActivity(accumulator, act);
240
+ return finishFold(accumulator);
241
+ }
242
+ /** Replay the activity stream once to fix every say's audience at landing. */
243
+ export function replayLandedAudiences(acts) {
244
+ const accumulator = createFoldAccumulator();
245
+ const byActivity = new Map();
246
+ const byIndex = new Map();
247
+ const lastJoinByKey = new Map();
248
+ for (const act of acts) {
249
+ if (act.kind === 'say') {
250
+ const recipients = recipientsAtLanding(accumulator.state, act);
251
+ byActivity.set(act, recipients);
252
+ const index = 'index' in act ? act.index : undefined;
253
+ if (typeof index === 'number')
254
+ byIndex.set(index, recipients);
255
+ }
256
+ if (act.kind === 'join' && 'index' in act && typeof act.index === 'number') {
257
+ lastJoinByKey.set(nameKey(act.actor), act.index);
258
+ }
259
+ applyActivity(accumulator, act);
260
+ }
261
+ const state = finishFold(accumulator);
262
+ const participants = state.participants.map((participant) => participant.name);
263
+ function recipientsFor(activity) {
264
+ const direct = byActivity.get(activity);
265
+ if (direct !== undefined)
266
+ return direct;
267
+ const index = 'index' in activity ? activity.index : undefined;
268
+ return typeof index === 'number' ? byIndex.get(index) ?? [] : [];
269
+ }
270
+ function resolveParticipant(name) {
271
+ return participants.find((participant) => sameName(participant, name));
272
+ }
273
+ return {
274
+ participants,
275
+ joined: state.joined,
276
+ replayedActivityCount: acts.length,
277
+ recipientsFor,
278
+ includes: (activity, participant) => activity.kind === 'say'
279
+ && !sameName(activity.actor, participant)
280
+ && recipientsFor(activity).some((recipient) => sameName(recipient, participant)),
281
+ lastJoinIndex: (participant) => lastJoinByKey.get(nameKey(participant)),
282
+ resolveParticipant,
283
+ };
284
+ }
236
285
  export function validate(state, act, options = {}) {
237
286
  const actor = actorOf(act);
238
287
  const current = actor === undefined ? undefined : currentParticipant(state, actor);
@@ -116,25 +116,36 @@ export function createFileCell(squarePath) {
116
116
  let closed = false;
117
117
  let version = 0;
118
118
  let fingerprint = fileFingerprint(squarePath);
119
+ let cached;
119
120
  function observe() {
120
121
  const next = fileFingerprint(squarePath);
121
- if (next === fingerprint)
122
- return;
123
- fingerprint = next;
124
- version += 1;
122
+ if (next !== fingerprint) {
123
+ fingerprint = next;
124
+ cached = undefined;
125
+ version += 1;
126
+ }
127
+ return fingerprint;
128
+ }
129
+ function currentState() {
130
+ const observed = observe();
131
+ if (cached?.fingerprint === observed)
132
+ return cloneState(cached.state);
133
+ const decoded = readSquareFile(squarePath);
134
+ cached = { fingerprint: observed, state: cloneState(decoded) };
135
+ return cloneState(cached.state);
125
136
  }
126
137
  return {
127
138
  async transact(fn) {
128
139
  assertCellOpen(closed);
129
140
  return withFileLock(`${squarePath}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, () => {
130
141
  assertCellOpen(closed);
131
- observe();
132
- const current = readSquareFile(squarePath);
142
+ const current = currentState();
133
143
  const working = cloneState(current);
134
144
  const outcome = fn(working, version);
135
145
  if (outcome.state !== undefined) {
136
146
  writeSquareSnapshot(squarePath, outcome.state);
137
147
  fingerprint = fileFingerprint(squarePath);
148
+ cached = { fingerprint, state: cloneState(outcome.state) };
138
149
  version += 1;
139
150
  }
140
151
  return outcome.result;
@@ -142,9 +153,7 @@ export function createFileCell(squarePath) {
142
153
  },
143
154
  async read() {
144
155
  assertCellOpen(closed);
145
- observe();
146
- const state = readSquareFile(squarePath);
147
- return { state: cloneState(state), version };
156
+ return { state: currentState(), version };
148
157
  },
149
158
  async changed(sinceVersion, timeoutMs) {
150
159
  assertCellOpen(closed);
package/dist/views.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type ActivityId } from './square-core.js';
2
2
  import { coreParticipants, coreStatus } from './decisions.js';
3
- import { type PlannedNotification } from './delivery.js';
3
+ import { type DeliveryModel, type PlannedNotification } from './delivery.js';
4
4
  import { type ActivitiesOptions, type ActivityObservation, type InboxNotification, type PublicAct, type RoomChangeAct, type SquareState, type StoredAct } from './model.js';
5
5
  import type { OpenSquare } from './open-square.js';
6
6
  import type { Activity, HistoryQuery, ParticipantStatus, PerceivedActivity, SquareSnapshot } from './square-facade.js';
@@ -96,6 +96,7 @@ export declare function watchPresentation(square: OpenSquare, name: string): Pro
96
96
  export declare function inboxProjection(square: OpenSquare, name: string, ownerId: string): Promise<InboxProjection>;
97
97
  export declare function streamProjection(square: OpenSquare, cursor: number, recipient?: string): Promise<StreamProjection>;
98
98
  export declare function notificationForAct(square: OpenSquare, actIndex: number): Promise<readonly PlannedNotification[]>;
99
+ export declare function pendingDeliveriesFromState(state: SquareState, delivery?: DeliveryModel): readonly PendingDeliveryProjection[];
99
100
  export declare function pendingDeliveries(square: OpenSquare): Promise<readonly PendingDeliveryProjection[]>;
100
101
  export declare function notificationEvidence(square: OpenSquare, recipient: string, actIndex: number): Promise<{
101
102
  readonly delivered: boolean;
package/dist/views.js CHANGED
@@ -1,16 +1,16 @@
1
1
  import { extractMentions, formatActivityId, parseActivityId } from './square-core.js';
2
2
  import { deliveryDelta, directedPeerSays } from './activity-feed.js';
3
3
  import { coreActivities, coreParticipants, coreStatus, resolveKnownName } from './decisions.js';
4
- import { deriveDeliveryModel, isActivitySeen, perceiveActivity, planActNotifications } from './delivery.js';
4
+ import { deriveDeliveryModel, isActivitySeen } from './delivery.js';
5
5
  import { SquareError, nameKey } from './model.js';
6
- import { countSays, currentHold, foldedState, freshWatchLease, inSquareCount, isCurrentlyJoined, observationFor, readCursor, resolveRosterName, rosterNames, watchTerminalStatus } from './runtime.js';
6
+ import { countSays, currentHold, foldedState, freshWatchLease, inSquareCount, isCurrentlyJoined, resolveRosterName, rosterNames, watchTerminalStatus } from './runtime.js';
7
7
  function expose(stored) {
8
8
  if (stored.kind === 'read' || stored.actor === undefined)
9
9
  throw new Error(`Cannot expose stored activity ${formatActivityId(stored.index)}`);
10
10
  return { id: formatActivityId(stored.index), at: stored.at, kind: stored.kind, actor: stored.actor, ...('body' in stored && stored.body !== undefined ? { body: stored.body } : {}), mentions: stored.kind === 'say' ? extractMentions(stored.body) : [], ...('target' in stored ? { target: stored.target } : {}), ...(stored.kind === 'say' && stored.reply !== undefined ? { reply: formatActivityId(stored.reply) } : {}) };
11
11
  }
12
- function exposePerceived(state, stored, viewer) {
13
- const perception = perceiveActivity(state, stored, viewer);
12
+ function exposePerceived(stored, viewer, delivery) {
13
+ const perception = delivery.perceive(stored, viewer);
14
14
  const activity = expose(stored);
15
15
  if (perception === 'full' || activity.body === undefined)
16
16
  return { ...activity, perception };
@@ -36,12 +36,16 @@ function selectHistory(stored, query) {
36
36
  return query.order === 'desc' ? selected.reverse() : selected;
37
37
  }
38
38
  function statuses(square, state) {
39
- return coreStatus(state, square.clock()).participants.filter((participant) => participant.state !== 'not joined').map((participant) => ({ name: participant.name, state: participant.state === 'done' ? 'done' : 'joined', consumedThrough: readCursor(state, participant.name) < 0 ? null : formatActivityId(readCursor(state, participant.name)), watching: participant.presence === 'watching', listening: participant.listening }));
39
+ const delivery = deriveDeliveryModel(state);
40
+ return coreStatus(state, square.clock(), delivery).participants.filter((participant) => participant.state !== 'not joined').map((participant) => {
41
+ const cursor = delivery.cursorFor(participant.name);
42
+ return { name: participant.name, state: participant.state === 'done' ? 'done' : 'joined', consumedThrough: cursor < 0 ? null : formatActivityId(cursor), watching: participant.presence === 'watching', listening: participant.listening };
43
+ });
40
44
  }
41
- function anchors(state) {
45
+ function anchors(state, delivery) {
42
46
  const result = {};
43
- for (const name of rosterNames(state)) {
44
- const activity = state.acts.findLast((candidate) => candidate.index <= readCursor(state, name) && (candidate.kind === 'say' || candidate.kind === 'done'));
47
+ for (const name of delivery.participants()) {
48
+ const activity = state.acts.findLast((candidate) => candidate.index <= delivery.cursorFor(name) && (candidate.kind === 'say' || candidate.kind === 'done'));
45
49
  if (activity !== undefined)
46
50
  result[activity.index] = [...(result[activity.index] ?? []), name];
47
51
  }
@@ -59,25 +63,26 @@ function sayNumbers(state) {
59
63
  return result;
60
64
  }
61
65
  export async function history(square, query = {}) { const { state } = await square.cell.read(); return selectHistory(coreActivities(state, historyOptions(query)), query).map(expose); }
62
- export async function participantHistory(square, name, query = {}) { const { state } = await square.cell.read(); const viewer = resolveKnownName(state, name); const effective = query.all === true || query.limit !== undefined ? query : { ...query, limit: 10 }; return selectHistory(coreActivities(state, historyOptions(effective, viewer)), effective).map((activity) => exposePerceived(state, activity, viewer)); }
66
+ export async function participantHistory(square, name, query = {}) { const { state } = await square.cell.read(); const viewer = resolveKnownName(state, name); const delivery = deriveDeliveryModel(state); const effective = query.all === true || query.limit !== undefined ? query : { ...query, limit: 10 }; return selectHistory(coreActivities(state, historyOptions(effective, viewer), delivery), effective).map((activity) => exposePerceived(activity, viewer, delivery)); }
63
67
  export async function resolveParticipant(square, name) { const { state } = await square.cell.read(); return { name: resolveKnownName(state, name), roster: rosterNames(state) }; }
64
68
  export async function currentParticipant(square, name) { const { state } = await square.cell.read(); const known = resolveRosterName(state, name); return known !== undefined && isCurrentlyJoined(state.acts, known) ? known : undefined; }
65
69
  export async function participants(square) { const { state } = await square.cell.read(); return statuses(square, state); }
66
70
  export async function snapshot(square) { const { state } = await square.cell.read(); const folded = foldedState(state); return { context: [...state.preamble, ...state.warmup].join('\n'), actCount: state.acts.filter((activity) => activity.kind !== 'read').length, hardCap: state.hardCap, ...(state.throttlePerMinute === undefined ? {} : { throttlePerMinute: state.throttlePerMinute }), held: folded.hold.active && folded.hold.actor !== undefined ? { by: folded.hold.actor, ...(folded.hold.reason === undefined ? {} : { reason: folded.hold.reason }) } : null, participants: statuses(square, state), delivered(name, id) { return isActivitySeen(state, name, parseRequiredActivityId(id)); } }; }
67
- export async function activityPresentation(square, name) { const { state } = await square.cell.read(); const known = resolveKnownName(state, name); const delta = deliveryDelta(state, known); const hold = currentHold(state.acts); return { name: known, roster: rosterNames(state), pendingPublic: directedPeerSays(state, delta, known), pendingRoomChanges: [], activities: state.acts, state, participantCount: inSquareCount(state), held: hold.active, ...(hold.reason === undefined ? {} : { holdReason: hold.reason }), ownActivityCount: countSays(state.acts, known), hardCap: state.hardCap }; }
71
+ export async function activityPresentation(square, name) { const { state } = await square.cell.read(); const known = resolveKnownName(state, name); const delivery = deriveDeliveryModel(state); const delta = deliveryDelta(state, known, delivery); const hold = currentHold(state.acts); return { name: known, roster: rosterNames(state), pendingPublic: directedPeerSays(state, delta, known, delivery), pendingRoomChanges: [], activities: state.acts, state, participantCount: inSquareCount(state), held: hold.active, ...(hold.reason === undefined ? {} : { holdReason: hold.reason }), ownActivityCount: countSays(state.acts, known), hardCap: state.hardCap }; }
68
72
  export async function entryPresentation(square, name, lastN = 10) { const { state } = await square.cell.read(); const known = resolveRosterName(state, name) ?? name; const publicActivities = state.acts.filter((activity) => activity.kind === 'say' || activity.kind === 'done'); return { joined: isCurrentlyJoined(state.acts, known), scene: state.warmup.join('\n').trim(), context: state.preamble.join('\n').trim(), joinContext: (state.preamble.at(-1) === '---' ? state.preamble.slice(0, -1) : state.preamble).join('\n').trim(), recentActivities: lastN === null ? publicActivities : publicActivities.slice(-lastN), state, sayNumbers: sayNumbers(state), participantCount: inSquareCount(state) }; }
69
- export async function historyPresentation(square, options) { const { state } = await square.cell.read(); return { activities: coreActivities(state, options).map((activity) => ({ ...activity, perception: options.viewer === undefined ? 'full' : perceiveActivity(state, activity, options.viewer) })), sayNumbers: sayNumbers(state), presenceAnchors: anchors(state), participantCount: inSquareCount(state) }; }
70
- export async function participantsPresentation(square) { const { state } = await square.cell.read(); return coreParticipants(state, square.clock()); }
73
+ export async function historyPresentation(square, options) { const { state } = await square.cell.read(); const delivery = deriveDeliveryModel(state); return { activities: coreActivities(state, options, delivery).map((activity) => ({ ...activity, perception: options.viewer === undefined ? 'full' : delivery.perceive(activity, options.viewer) })), sayNumbers: sayNumbers(state), presenceAnchors: anchors(state, delivery), participantCount: inSquareCount(state) }; }
74
+ export async function participantsPresentation(square) { const { state } = await square.cell.read(); const delivery = deriveDeliveryModel(state); return coreParticipants(state, square.clock(), delivery); }
71
75
  export async function listPresentation(square) { const { state } = await square.cell.read(); return { context: state.preamble, participants: foldedState(state).participants.filter((participant) => participant.joined).sort((left, right) => (right.lastActiveAt ?? -Infinity) - (left.lastActiveAt ?? -Infinity) || left.name.localeCompare(right.name)).map((participant) => participant.name), activities: state.acts.filter((activity) => activity.kind === 'say').length }; }
72
- export async function statusPresentation(square) { const { state } = await square.cell.read(); const status = coreStatus(state, square.clock()); return { state, status, ...(status.latestAct?.kind === 'say' ? { latestActNumber: countSays(state.acts, status.latestAct.actor) } : {}) }; }
76
+ export async function statusPresentation(square) { const { state } = await square.cell.read(); const delivery = deriveDeliveryModel(state); const status = coreStatus(state, square.clock(), delivery); return { state, status, ...(status.latestAct?.kind === 'say' ? { latestActNumber: countSays(state.acts, status.latestAct.actor) } : {}) }; }
73
77
  export async function eventPresentation(square, id) { const { state } = await square.cell.read(); const activity = state.acts.find((candidate) => candidate.index === parseRequiredActivityId(id)); if (activity === undefined)
74
78
  throw new SquareError('invalid_args', `Unknown activity id: ${id}`); return { activity, participantCount: inSquareCount(state), held: currentHold(state.acts).active }; }
75
- export async function watchPresentation(square, name) { const { state } = await square.cell.read(); const known = resolveKnownName(state, name); const now = square.clock(); const terminal = watchTerminalStatus(state, known); return { activities: state.acts, state, participantCount: inSquareCount(state), presence: { participants: coreParticipants(state, now), now }, ...(terminal === undefined ? {} : { terminalStatus: terminal }) }; }
76
- export async function inboxProjection(square, name, ownerId) { const { state } = await square.cell.read(); const known = resolveRosterName(state, name); if (known === undefined || !isCurrentlyJoined(state.acts, known))
77
- return { name, joined: false, notifications: [] }; const lease = freshWatchLease(state, known, square.clock()); return { name: known, joined: true, notifications: deriveDeliveryModel(state).pendingFor(known).map(({ item, route }) => ({ actIndex: item.index, actor: item.actor, at: item.at, route, body: item.body })), ...(lease?.ownerId === ownerId ? { catchLease: lease } : {}) }; }
78
- export async function streamProjection(square, cursor, recipient) { const { state } = await square.cell.read(); return { activities: state.acts.filter((activity) => activity.index > cursor).flatMap((activity) => { if (recipient === undefined)
79
- return [{ activity }]; const notification = planActNotifications(state, activity).find((candidate) => nameKey(candidate.recipient) === nameKey(recipient)); return notification === undefined ? [] : [{ activity, route: notification.route }]; }), cursor: Math.max(cursor, ...state.acts.map((activity) => activity.index)) }; }
80
- export async function notificationForAct(square, actIndex) { const { state } = await square.cell.read(); const activity = state.acts.find((candidate) => candidate.index === actIndex); return activity === undefined ? [] : planActNotifications(state, activity); }
81
- export async function pendingDeliveries(square) { const { state } = await square.cell.read(); return [...new Set(state.acts.filter((activity) => activity.kind === 'join').map((activity) => activity.actor))].filter((name) => isCurrentlyJoined(state.acts, name)).map((recipient) => ({ recipient, notifications: deriveDeliveryModel(state).pendingFor(recipient) })); }
82
- export async function notificationEvidence(square, recipient, actIndex) { const { state } = await square.cell.read(); return { delivered: isActivitySeen(state, recipient, actIndex), observation: observationFor(state, recipient, actIndex) }; }
83
- export async function notificationDelivered(square, recipient, actIndex) { const { state } = await square.cell.read(); return isActivitySeen(state, recipient, actIndex); }
79
+ export async function watchPresentation(square, name) { const { state } = await square.cell.read(); const known = resolveKnownName(state, name); const now = square.clock(); const terminal = watchTerminalStatus(state, known); const delivery = deriveDeliveryModel(state); return { activities: state.acts, state, participantCount: inSquareCount(state), presence: { participants: coreParticipants(state, now, delivery), now }, ...(terminal === undefined ? {} : { terminalStatus: terminal }) }; }
80
+ export async function inboxProjection(square, name, ownerId) { const { state } = await square.cell.read(); const delivery = deriveDeliveryModel(state); const known = delivery.knownParticipant(name); if (known === undefined || !delivery.joinedRecipients().some((recipient) => nameKey(recipient) === nameKey(known)))
81
+ return { name, joined: false, notifications: [] }; const lease = freshWatchLease(state, known, square.clock()); return { name: known, joined: true, notifications: delivery.pendingFor(known).map(({ item, route }) => ({ actIndex: item.index, actor: item.actor, at: item.at, route, body: item.body })), ...(lease?.ownerId === ownerId ? { catchLease: lease } : {}) }; }
82
+ export async function streamProjection(square, cursor, recipient) { const { state } = await square.cell.read(); const delivery = recipient === undefined ? undefined : deriveDeliveryModel(state); return { activities: state.acts.filter((activity) => activity.index > cursor).flatMap((activity) => { if (delivery === undefined || recipient === undefined)
83
+ return [{ activity }]; const notification = delivery.plan(activity).find((candidate) => nameKey(candidate.recipient) === nameKey(recipient)); return notification === undefined ? [] : [{ activity, route: notification.route }]; }), cursor: Math.max(cursor, ...state.acts.map((activity) => activity.index)) }; }
84
+ export async function notificationForAct(square, actIndex) { const { state } = await square.cell.read(); const activity = state.acts.find((candidate) => candidate.index === actIndex); return activity === undefined ? [] : deriveDeliveryModel(state).plan(activity); }
85
+ export function pendingDeliveriesFromState(state, delivery = deriveDeliveryModel(state)) { return delivery.joinedRecipients().map((recipient) => ({ recipient, notifications: delivery.pendingFor(recipient) })); }
86
+ export async function pendingDeliveries(square) { const { state } = await square.cell.read(); return pendingDeliveriesFromState(state); }
87
+ export async function notificationEvidence(square, recipient, actIndex) { const { state } = await square.cell.read(); const delivery = deriveDeliveryModel(state); const known = delivery.knownParticipant(recipient) ?? recipient; return { delivered: delivery.isSeen(known, actIndex), observation: state.runtime.observations?.[known]?.[formatActivityId(actIndex)] }; }
88
+ export async function notificationDelivered(square, recipient, actIndex) { const { state } = await square.cell.read(); return deriveDeliveryModel(state).isSeen(recipient, actIndex); }
@@ -1,4 +1,6 @@
1
1
  import { type WakeRoute } from './model.js';
2
+ import { type DeliveryModel } from './delivery.js';
3
+ import { type SquareState } from './model.js';
2
4
  import { type WakeAttempt } from './wake-attempts.js';
3
5
  export interface WakeEvidence {
4
6
  delivered: boolean;
@@ -8,6 +10,12 @@ export interface WakeEvidence {
8
10
  terminal?: WakeAttempt;
9
11
  attemptableRoutes: WakeRoute[];
10
12
  }
13
+ export interface WakeEvidenceProjection {
14
+ evidence(recipient: string, actIndex: number): WakeEvidence;
15
+ }
16
+ /** Capture the primary wake facts once and derive any number of eligibility decisions from them. */
17
+ export declare function wakeEvidenceProjection(squarePath: string, now: number, env: NodeJS.ProcessEnv): Promise<WakeEvidenceProjection>;
18
+ export declare function wakeEvidenceProjectionFromState(squarePath: string, state: SquareState, now: number, env: NodeJS.ProcessEnv, delivery?: DeliveryModel): WakeEvidenceProjection;
11
19
  /** Project every wake decision from the same primary evidence. */
12
20
  export declare function wakeEvidence(squarePath: string, recipient: string, actIndex: number, now: number, env: NodeJS.ProcessEnv): Promise<WakeEvidence>;
13
21
  export declare function wakeIsEligible(evidence: WakeEvidence): boolean;