@astrosheep/square 0.3.24 → 0.3.26
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/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity-feed.d.ts +2 -2
- package/dist/activity-feed.js +5 -7
- package/dist/cli/observation-commands.js +9 -3
- package/dist/decisions.d.ts +4 -3
- package/dist/decisions.js +22 -20
- package/dist/delivery.d.ts +11 -3
- package/dist/delivery.js +42 -22
- package/dist/notifications.d.ts +4 -1
- package/dist/notifications.js +19 -14
- package/dist/presence.d.ts +2 -1
- package/dist/presence.js +10 -11
- package/dist/presentation.d.ts +2 -1
- package/dist/presentation.js +2 -1
- package/dist/runtime.d.ts +2 -2
- package/dist/runtime.js +7 -6
- package/dist/square-core.d.ts +11 -5
- package/dist/square-core.js +138 -89
- package/dist/views.d.ts +2 -2
- package/dist/views.js +26 -26
- package/dist/wake-evidence.d.ts +2 -1
- package/dist/wake-evidence.js +5 -5
- package/dist/watch.d.ts +2 -1
- package/dist/watch.js +12 -6
- package/extensions/square-pi.js +10 -8
- package/package.json +1 -1
package/dist/activity-feed.d.ts
CHANGED
|
@@ -5,10 +5,10 @@ export interface ActivityFeedFilter {
|
|
|
5
5
|
}
|
|
6
6
|
export declare function actDelta(acts: StoredAct[], cursor: number): StoredAct[];
|
|
7
7
|
/** Visible activities after the participant's derived continuous-seen prefix. */
|
|
8
|
-
export declare function deliveryDelta(squareState: SquareState, name: string): StoredAct[];
|
|
8
|
+
export declare function deliveryDelta(squareState: SquareState, name: string, delivery?: import("./delivery.js").DeliveryModel): StoredAct[];
|
|
9
9
|
export declare function peerRoomChanges(delta: StoredAct[], name: string): RoomChangeAct[];
|
|
10
10
|
export declare function peerPublicActs(delta: StoredAct[], name: string): PublicAct[];
|
|
11
|
-
export declare function directedPeerSays(squareState: SquareState, delta: StoredAct[], name: string): Extract<StoredAct, {
|
|
11
|
+
export declare function directedPeerSays(squareState: SquareState, delta: StoredAct[], name: string, delivery?: import("./delivery.js").DeliveryModel): Extract<StoredAct, {
|
|
12
12
|
kind: 'say';
|
|
13
13
|
}>[];
|
|
14
14
|
export declare function matchesFeedFilter(act: StoredAct, filter: ActivityFeedFilter, recipients?: readonly string[]): boolean;
|
package/dist/activity-feed.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { sameName } from './model.js';
|
|
2
|
-
import {
|
|
3
|
-
import { readCursor } from './runtime.js';
|
|
4
|
-
import { matchesCatchFilter } from './delivery.js';
|
|
2
|
+
import { deriveDeliveryModel, matchesCatchFilter } from './delivery.js';
|
|
5
3
|
export function actDelta(acts, cursor) {
|
|
6
4
|
return acts.filter((act) => act.index > cursor);
|
|
7
5
|
}
|
|
8
6
|
/** Visible activities after the participant's derived continuous-seen prefix. */
|
|
9
|
-
export function deliveryDelta(squareState, name) {
|
|
10
|
-
return actDelta(squareState.acts,
|
|
7
|
+
export function deliveryDelta(squareState, name, delivery = deriveDeliveryModel(squareState)) {
|
|
8
|
+
return actDelta(squareState.acts, delivery.cursorFor(name));
|
|
11
9
|
}
|
|
12
10
|
export function peerRoomChanges(delta, name) {
|
|
13
11
|
return delta.filter((act) => act.actor !== undefined && !sameName(act.actor, name) && act.kind !== 'say' && act.kind !== 'read');
|
|
@@ -15,8 +13,8 @@ export function peerRoomChanges(delta, name) {
|
|
|
15
13
|
export function peerPublicActs(delta, name) {
|
|
16
14
|
return delta.filter((act) => act.actor !== undefined && !sameName(act.actor, name) && (act.kind === 'say' || act.kind === 'done'));
|
|
17
15
|
}
|
|
18
|
-
export function directedPeerSays(squareState, delta, name) {
|
|
19
|
-
return delta.filter((act) =>
|
|
16
|
+
export function directedPeerSays(squareState, delta, name, delivery = deriveDeliveryModel(squareState)) {
|
|
17
|
+
return delta.filter((act) => delivery.directedTo(act, name));
|
|
20
18
|
}
|
|
21
19
|
function matchesParticipants(act, participants) {
|
|
22
20
|
return participants === undefined || (act.actor !== undefined && participants.some((participant) => sameName(participant, act.actor)));
|
|
@@ -13,6 +13,7 @@ import { openSquare } from '../square-file-adapter.js';
|
|
|
13
13
|
import { closeOpenSquare } from '../open-square.js';
|
|
14
14
|
import { historyPresentation, participantsPresentation, statusPresentation } from '../views.js';
|
|
15
15
|
import { fail, parseDurationMs, parseNameList, parseNonNegativeInteger, readStdinSync, requireParticipant, requireSquarePath, requireValue, usage, } from './context.js';
|
|
16
|
+
const STATUS_PARTICIPANT_PREVIEW_LIMIT = 10;
|
|
16
17
|
export const listCommand = {
|
|
17
18
|
parse: (argv) => argv,
|
|
18
19
|
async execute(argv, context) {
|
|
@@ -95,8 +96,9 @@ export const catchCommand = {
|
|
|
95
96
|
},
|
|
96
97
|
async execute(intent, context) {
|
|
97
98
|
const squarePath = requireSquarePath(context);
|
|
98
|
-
await cmdWatch(squarePath, requireParticipant(context.name), intent);
|
|
99
|
-
|
|
99
|
+
const caught = await cmdWatch(squarePath, requireParticipant(context.name), intent);
|
|
100
|
+
if (caught !== false)
|
|
101
|
+
await sweepPendingNotifications(squarePath);
|
|
100
102
|
},
|
|
101
103
|
present: () => { },
|
|
102
104
|
};
|
|
@@ -382,7 +384,7 @@ export const statusCommand = {
|
|
|
382
384
|
return aViewer ? -1 : 1;
|
|
383
385
|
return (b.lastActiveAt ?? -Infinity) - (a.lastActiveAt ?? -Infinity) || a.name.localeCompare(b.name);
|
|
384
386
|
});
|
|
385
|
-
const people = active.length === 0 ? [' ○ nobody in the square'] : active.map((participant) => {
|
|
387
|
+
const people = active.length === 0 ? [' ○ nobody in the square'] : active.slice(0, STATUS_PARTICIPANT_PREVIEW_LIMIT).map((participant) => {
|
|
386
388
|
const glyph = participant.presence === 'watching'
|
|
387
389
|
? '◎'
|
|
388
390
|
: participant.activityCount > 0 ? '●' : '○';
|
|
@@ -401,6 +403,10 @@ export const statusCommand = {
|
|
|
401
403
|
: 'caught up';
|
|
402
404
|
return ` ${glyph} ${participantIdentity(participant.name)} · ${summary}${attention === '' ? '' : ` · ${attention}`}`;
|
|
403
405
|
});
|
|
406
|
+
if (active.length > STATUS_PARTICIPANT_PREVIEW_LIMIT) {
|
|
407
|
+
people.push(` ○ … ${active.length - STATUS_PARTICIPANT_PREVIEW_LIMIT} more participants`);
|
|
408
|
+
people.push(`» ${commandPrefix(squarePath)} participants`);
|
|
409
|
+
}
|
|
404
410
|
const cap = result.hardCap === null ? 'unlimited' : String(result.hardCap);
|
|
405
411
|
const hold = result.holdActive
|
|
406
412
|
? `· ${result.holdActor === undefined ? 'someone' : participantIdentity(result.holdActor)} raised a hand${result.holdReason ? ` — ${result.holdReason}` : ''} · ${result.holdAt === undefined
|
package/dist/decisions.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ActivitiesOptions, type Act, type StoredAct, type SquareState, type Reach, type HardCap } from './model.js';
|
|
2
2
|
import { directedPeerSays, peerRoomChanges } from './activity-feed.js';
|
|
3
3
|
import { type Perception } from './square-core.js';
|
|
4
|
+
import { type DeliveryModel } from './delivery.js';
|
|
4
5
|
export interface UnreadActivitySummary {
|
|
5
6
|
name: string;
|
|
6
7
|
count: number;
|
|
@@ -106,6 +107,6 @@ export interface StatusResult {
|
|
|
106
107
|
latestAct: StoredAct | undefined;
|
|
107
108
|
now: number;
|
|
108
109
|
}
|
|
109
|
-
export declare function coreStatus(squareState: SquareState, now: number): StatusResult;
|
|
110
|
-
export declare function coreParticipants(squareState: SquareState, now: number): ParticipantStatus[];
|
|
111
|
-
export declare function coreActivities(squareState: SquareState, opts: ActivitiesOptions): StoredAct[];
|
|
110
|
+
export declare function coreStatus(squareState: SquareState, now: number, delivery?: DeliveryModel): StatusResult;
|
|
111
|
+
export declare function coreParticipants(squareState: SquareState, now: number, delivery?: DeliveryModel): ParticipantStatus[];
|
|
112
|
+
export declare function coreActivities(squareState: SquareState, opts: ActivitiesOptions, suppliedDelivery?: DeliveryModel): StoredAct[];
|
package/dist/decisions.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { SquareError, sameName, validateName, } from './model.js';
|
|
2
2
|
import { participantIdentity } from './participant-identity.js';
|
|
3
|
-
import { UNREAD_BLOCK_GRACE_MS, actId, actStableIndex, foldedState, freshWatchLease, publicActs,
|
|
3
|
+
import { UNREAD_BLOCK_GRACE_MS, actId, actStableIndex, foldedState, freshWatchLease, publicActs, resolveRosterName, rosterNames, THROTTLE_WINDOW_MS, } from './runtime.js';
|
|
4
4
|
import { actDelta, directedPeerSays } from './activity-feed.js';
|
|
5
5
|
import { formatActivityId, isIgnored, isListening, listeningTo, validate } from './square-core.js';
|
|
6
|
-
import { deriveDeliveryModel
|
|
6
|
+
import { deriveDeliveryModel } from './delivery.js';
|
|
7
7
|
import { compileSearchPattern } from './search.js';
|
|
8
8
|
export function resolveKnownName(squareState, name) {
|
|
9
9
|
validateName(name);
|
|
@@ -100,8 +100,9 @@ export function decideAct(squareState, input) {
|
|
|
100
100
|
if (result.reason === 'not_joined')
|
|
101
101
|
throw new SquareError('not_joined', `${name} has not joined this square`);
|
|
102
102
|
}
|
|
103
|
-
const
|
|
104
|
-
const
|
|
103
|
+
const delivery = deriveDeliveryModel(squareState);
|
|
104
|
+
const delta = actDelta(squareState.acts, delivery.cursorFor(name));
|
|
105
|
+
const unreadPublic = directedPeerSays(squareState, delta, name, delivery);
|
|
105
106
|
const unreadRoomChanges = [];
|
|
106
107
|
const sayCountByActor = new Map();
|
|
107
108
|
const unreadByParticipant = new Map();
|
|
@@ -119,7 +120,7 @@ export function decideAct(squareState, input) {
|
|
|
119
120
|
latestAt: currentSummary === undefined ? item.at : Math.max(currentSummary.latestAt, item.at),
|
|
120
121
|
previews: [
|
|
121
122
|
...(currentSummary?.previews ?? []),
|
|
122
|
-
{ number: sayCountByActor.get(actorKey) ?? 1, act: item, perception:
|
|
123
|
+
{ number: sayCountByActor.get(actorKey) ?? 1, act: item, perception: delivery.perceive(item, name) },
|
|
123
124
|
].slice(-UNREAD_PREVIEW_LIMIT),
|
|
124
125
|
});
|
|
125
126
|
}
|
|
@@ -189,10 +190,10 @@ export function coreResume(squareState, actor, now) {
|
|
|
189
190
|
requireStanding(squareState, act);
|
|
190
191
|
return act;
|
|
191
192
|
}
|
|
192
|
-
function presenceFor(squareState, snapshot, name, now) {
|
|
193
|
+
function presenceFor(squareState, snapshot, name, now, delivery) {
|
|
193
194
|
if (snapshot?.done)
|
|
194
195
|
return { state: 'done', lastAt: snapshot.lastActiveAt };
|
|
195
|
-
const cursorAt = squareState.acts.findLast((act) => act.index <=
|
|
196
|
+
const cursorAt = squareState.acts.findLast((act) => act.index <= delivery.cursorFor(name))?.at;
|
|
196
197
|
const lease = freshWatchLease(squareState, name, now);
|
|
197
198
|
if (lease !== undefined)
|
|
198
199
|
return { state: 'watching', lastAt: cursorAt ?? lease.heartbeatAt };
|
|
@@ -201,19 +202,19 @@ function presenceFor(squareState, snapshot, name, now) {
|
|
|
201
202
|
? { state: 'never-joined', lastAt: undefined }
|
|
202
203
|
: { state: 'active', lastAt };
|
|
203
204
|
}
|
|
204
|
-
function buildParticipantStatuses(squareState, now, state = foldedState(squareState)) {
|
|
205
|
-
const delivery = deriveDeliveryModel(squareState);
|
|
205
|
+
function buildParticipantStatuses(squareState, now, state = foldedState(squareState), suppliedDelivery) {
|
|
206
|
+
const delivery = suppliedDelivery ?? deriveDeliveryModel(squareState);
|
|
206
207
|
return state.participants.map((snapshot) => {
|
|
207
208
|
const participant = snapshot.name;
|
|
208
|
-
const presence = presenceFor(squareState, snapshot, participant, now);
|
|
209
|
+
const presence = presenceFor(squareState, snapshot, participant, now, delivery);
|
|
209
210
|
const participantStatus = snapshot?.done ? 'done' : snapshot?.joined ? 'active' : 'not joined';
|
|
210
|
-
const consumedThrough =
|
|
211
|
+
const consumedThrough = delivery.cursorFor(participant);
|
|
211
212
|
let unreadActivityCount = 0;
|
|
212
213
|
if (snapshot?.joined) {
|
|
213
214
|
for (const act of squareState.acts) {
|
|
214
215
|
if (actStableIndex(act) <= consumedThrough)
|
|
215
216
|
continue;
|
|
216
|
-
if (directedPeerSays(squareState, [act], participant).length > 0)
|
|
217
|
+
if (directedPeerSays(squareState, [act], participant, delivery).length > 0)
|
|
217
218
|
unreadActivityCount++;
|
|
218
219
|
}
|
|
219
220
|
}
|
|
@@ -230,7 +231,7 @@ function buildParticipantStatuses(squareState, now, state = foldedState(squareSt
|
|
|
230
231
|
};
|
|
231
232
|
});
|
|
232
233
|
}
|
|
233
|
-
export function coreStatus(squareState, now) {
|
|
234
|
+
export function coreStatus(squareState, now, delivery) {
|
|
234
235
|
const state = foldedState(squareState);
|
|
235
236
|
const latestAct = publicActs(squareState.acts).at(-1);
|
|
236
237
|
return {
|
|
@@ -242,19 +243,21 @@ export function coreStatus(squareState, now) {
|
|
|
242
243
|
holdReason: state.hold.reason,
|
|
243
244
|
holdActor: state.hold.actor,
|
|
244
245
|
holdAt: state.hold.at,
|
|
245
|
-
participants: buildParticipantStatuses(squareState, now, state),
|
|
246
|
+
participants: buildParticipantStatuses(squareState, now, state, delivery),
|
|
246
247
|
latestAct,
|
|
247
248
|
now,
|
|
248
249
|
};
|
|
249
250
|
}
|
|
250
|
-
export function coreParticipants(squareState, now) {
|
|
251
|
-
return buildParticipantStatuses(squareState, now);
|
|
251
|
+
export function coreParticipants(squareState, now, delivery) {
|
|
252
|
+
return buildParticipantStatuses(squareState, now, undefined, delivery);
|
|
252
253
|
}
|
|
253
|
-
export function coreActivities(squareState, opts) {
|
|
254
|
+
export function coreActivities(squareState, opts, suppliedDelivery) {
|
|
254
255
|
const participants = opts.participants ?? [];
|
|
255
256
|
const canonicalParticipants = participants.map((participant) => resolveKnownName(squareState, participant));
|
|
256
257
|
const viewer = opts.viewer !== undefined ? resolveKnownName(squareState, opts.viewer) : undefined;
|
|
257
258
|
let acts = [...squareState.acts];
|
|
259
|
+
let delivery = suppliedDelivery;
|
|
260
|
+
const projected = () => delivery ??= deriveDeliveryModel(squareState);
|
|
258
261
|
// --at establishes one or more context windows first; other filters AND inside their union.
|
|
259
262
|
if (opts.atIndexes !== undefined && opts.atIndexes.length > 0) {
|
|
260
263
|
const before = opts.beforeContext ?? 0;
|
|
@@ -280,13 +283,12 @@ export function coreActivities(squareState, opts) {
|
|
|
280
283
|
acts = acts.filter((act) => act.at > opts.after);
|
|
281
284
|
if (opts.mention != null) {
|
|
282
285
|
const mention = resolveKnownName(squareState, opts.mention);
|
|
283
|
-
|
|
284
|
-
acts = acts.filter((act) => act.kind === 'say' && delivery.plan(act).some((item) => sameName(item.recipient, mention)));
|
|
286
|
+
acts = acts.filter((act) => act.kind === 'say' && projected().plan(act).some((item) => sameName(item.recipient, mention)));
|
|
285
287
|
}
|
|
286
288
|
if (opts.pending) {
|
|
287
289
|
if (viewer === undefined)
|
|
288
290
|
return [];
|
|
289
|
-
const pendingIndexes = new Set(
|
|
291
|
+
const pendingIndexes = new Set(projected().pendingFor(viewer).map((notification) => notification.item.index));
|
|
290
292
|
acts = acts.filter((act) => pendingIndexes.has(act.index));
|
|
291
293
|
}
|
|
292
294
|
const search = opts.grep !== undefined ? { pattern: opts.grep, fixed: false } : opts.fixed !== undefined ? { pattern: opts.fixed, fixed: true } : undefined;
|
package/dist/delivery.d.ts
CHANGED
|
@@ -57,6 +57,14 @@ export interface CatchFilterShape {
|
|
|
57
57
|
export interface DeliveryModel {
|
|
58
58
|
plan(item: StoredAct): PlannedNotification[];
|
|
59
59
|
pendingFor(recipient: string): PlannedNotification[];
|
|
60
|
+
directedTo(item: StoredAct, recipient: string): boolean;
|
|
61
|
+
perceive(item: StoredAct, viewer: string): Perception;
|
|
62
|
+
cursorFor(recipient: string): number;
|
|
63
|
+
isSeen(recipient: string, actOrIndex: StoredAct | number): boolean;
|
|
64
|
+
knownParticipant(name: string): string | undefined;
|
|
65
|
+
participants(): readonly string[];
|
|
66
|
+
joinedRecipients(): readonly string[];
|
|
67
|
+
readonly replayedActivityCount: number;
|
|
60
68
|
}
|
|
61
69
|
export declare function isActivitySeen(squareState: SquareState, name: string, actOrIndex: StoredAct | number): boolean;
|
|
62
70
|
/**
|
|
@@ -64,10 +72,10 @@ export declare function isActivitySeen(squareState: SquareState, name: string, a
|
|
|
64
72
|
* All consumers share these targets instead of reinterpreting artifact text or cursor state.
|
|
65
73
|
*/
|
|
66
74
|
export declare function deriveDeliveryModel(squareState: SquareState): DeliveryModel;
|
|
67
|
-
export declare function planActNotifications(squareState: SquareState, item: StoredAct): PlannedNotification[];
|
|
68
|
-
export declare function perceiveActivity(squareState: SquareState, item: StoredAct, viewer: string): Perception;
|
|
75
|
+
export declare function planActNotifications(squareState: SquareState, item: StoredAct, delivery?: DeliveryModel): PlannedNotification[];
|
|
76
|
+
export declare function perceiveActivity(squareState: SquareState, item: StoredAct, viewer: string, delivery?: DeliveryModel): Perception;
|
|
69
77
|
/** Mark only the notifications selected by the canonical catch projection as fully seen. */
|
|
70
|
-
export declare function markSeenNotifications(squareState: SquareState, recipient: string, delivered: StoredAct[], at?: number): boolean;
|
|
78
|
+
export declare function markSeenNotifications(squareState: SquareState, recipient: string, delivered: StoredAct[], at?: number, delivery?: DeliveryModel): boolean;
|
|
71
79
|
/** Canonical say-activity filter shared by catch selection and hook ownership. */
|
|
72
80
|
export declare function matchesCatchFilter(activity: CatchFilterShape, filter: WatchLeaseFilter): boolean;
|
|
73
81
|
/** True only when the live catch's own filters would deliver this notification. */
|
package/dist/delivery.js
CHANGED
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
import { findParticipantName, sameName, } from './model.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { audienceOf, formatActivityId, replayLandedAudiences } from './square-core.js';
|
|
3
|
+
import { matchesMentionTarget, readCursor, recordObservation } from './runtime.js';
|
|
4
4
|
export function notificationMessageId(squarePath, actIndex) {
|
|
5
5
|
return `square:${squarePath}#${formatActivityId(actIndex)}`;
|
|
6
6
|
}
|
|
7
|
-
function canonicalRecipient(squareState, name) {
|
|
8
|
-
return resolveRosterName(squareState, name) ?? name;
|
|
9
|
-
}
|
|
10
7
|
export function isActivitySeen(squareState, name, actOrIndex) {
|
|
11
|
-
|
|
12
|
-
return observationFor(squareState, canonicalRecipient(squareState, name), index)?.state === 'seen';
|
|
8
|
+
return deriveDeliveryModel(squareState).isSeen(name, actOrIndex);
|
|
13
9
|
}
|
|
14
10
|
/**
|
|
15
11
|
* Derive delivery behavior once from the decoded Square state.
|
|
16
12
|
* All consumers share these targets instead of reinterpreting artifact text or cursor state.
|
|
17
13
|
*/
|
|
18
14
|
export function deriveDeliveryModel(squareState) {
|
|
19
|
-
const
|
|
15
|
+
const landed = replayLandedAudiences(squareState.acts);
|
|
16
|
+
const roster = [...landed.joined];
|
|
20
17
|
const plannedByIndex = new Map();
|
|
21
18
|
let pendingByRecipient;
|
|
19
|
+
function canonicalRecipient(name) {
|
|
20
|
+
return landed.resolveParticipant(name) ?? name;
|
|
21
|
+
}
|
|
22
|
+
function isSeen(requestedRecipient, actOrIndex) {
|
|
23
|
+
const recipient = canonicalRecipient(requestedRecipient);
|
|
24
|
+
const index = typeof actOrIndex === 'number' ? actOrIndex : actOrIndex.index;
|
|
25
|
+
return squareState.runtime.observations?.[recipient]?.[formatActivityId(index)]?.state === 'seen';
|
|
26
|
+
}
|
|
22
27
|
function plan(item) {
|
|
23
28
|
if (item.kind !== 'say')
|
|
24
29
|
return [];
|
|
@@ -27,7 +32,7 @@ export function deriveDeliveryModel(squareState) {
|
|
|
27
32
|
return [...cached];
|
|
28
33
|
const sayItem = item;
|
|
29
34
|
const audience = audienceOf(sayItem);
|
|
30
|
-
const recipients =
|
|
35
|
+
const recipients = landed.recipientsFor(sayItem);
|
|
31
36
|
const planned = recipients.map((recipient) => {
|
|
32
37
|
const route = audience.kind === 'bell'
|
|
33
38
|
? 'bell'
|
|
@@ -43,7 +48,7 @@ export function deriveDeliveryModel(squareState) {
|
|
|
43
48
|
return [];
|
|
44
49
|
if (pendingByRecipient === undefined) {
|
|
45
50
|
pendingByRecipient = new Map(roster.map((name) => [name, []]));
|
|
46
|
-
const joinedAfter = new Map(roster.map((name) => [name, lastJoinIndex(
|
|
51
|
+
const joinedAfter = new Map(roster.map((name) => [name, landed.lastJoinIndex(name)]));
|
|
47
52
|
for (const act of squareState.acts) {
|
|
48
53
|
if (act.kind !== 'say')
|
|
49
54
|
continue;
|
|
@@ -51,7 +56,7 @@ export function deriveDeliveryModel(squareState) {
|
|
|
51
56
|
const joinedAt = joinedAfter.get(planned.recipient);
|
|
52
57
|
if (joinedAt === undefined || act.index <= joinedAt)
|
|
53
58
|
continue;
|
|
54
|
-
if (
|
|
59
|
+
if (isSeen(planned.recipient, act.index))
|
|
55
60
|
continue;
|
|
56
61
|
pendingByRecipient.get(planned.recipient)?.push(planned);
|
|
57
62
|
}
|
|
@@ -59,23 +64,38 @@ export function deriveDeliveryModel(squareState) {
|
|
|
59
64
|
}
|
|
60
65
|
return [...(pendingByRecipient.get(recipient) ?? [])];
|
|
61
66
|
}
|
|
62
|
-
|
|
67
|
+
function directedTo(item, recipient) {
|
|
68
|
+
return item.kind === 'say' && landed.includes(item, recipient);
|
|
69
|
+
}
|
|
70
|
+
function perceive(item, viewer) {
|
|
71
|
+
if (item.kind !== 'say' || sameName(item.actor, viewer))
|
|
72
|
+
return 'full';
|
|
73
|
+
return directedTo(item, viewer) ? 'full' : 'presence';
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
plan,
|
|
77
|
+
pendingFor,
|
|
78
|
+
directedTo,
|
|
79
|
+
perceive,
|
|
80
|
+
cursorFor: (recipient) => readCursor(squareState, recipient, landed),
|
|
81
|
+
isSeen,
|
|
82
|
+
knownParticipant: (name) => landed.resolveParticipant(name),
|
|
83
|
+
participants: () => landed.participants,
|
|
84
|
+
joinedRecipients: () => roster,
|
|
85
|
+
replayedActivityCount: landed.replayedActivityCount,
|
|
86
|
+
};
|
|
63
87
|
}
|
|
64
|
-
export function planActNotifications(squareState, item) {
|
|
65
|
-
return
|
|
88
|
+
export function planActNotifications(squareState, item, delivery = deriveDeliveryModel(squareState)) {
|
|
89
|
+
return delivery.plan(item);
|
|
66
90
|
}
|
|
67
|
-
export function perceiveActivity(squareState, item, viewer) {
|
|
68
|
-
|
|
69
|
-
return 'full';
|
|
70
|
-
return deriveDeliveryModel(squareState).plan(item).some((planned) => sameName(planned.recipient, viewer))
|
|
71
|
-
? 'full'
|
|
72
|
-
: 'presence';
|
|
91
|
+
export function perceiveActivity(squareState, item, viewer, delivery = deriveDeliveryModel(squareState)) {
|
|
92
|
+
return delivery.perceive(item, viewer);
|
|
73
93
|
}
|
|
74
94
|
/** Mark only the notifications selected by the canonical catch projection as fully seen. */
|
|
75
|
-
export function markSeenNotifications(squareState, recipient, delivered, at = Date.now()) {
|
|
95
|
+
export function markSeenNotifications(squareState, recipient, delivered, at = Date.now(), delivery = deriveDeliveryModel(squareState)) {
|
|
76
96
|
const deliveredIndexes = new Set(delivered.map((item) => item.index));
|
|
77
97
|
let changed = false;
|
|
78
|
-
for (const notification of
|
|
98
|
+
for (const notification of delivery.pendingFor(recipient)) {
|
|
79
99
|
if (!deliveredIndexes.has(notification.item.index))
|
|
80
100
|
continue;
|
|
81
101
|
changed = recordObservation(squareState, notification.recipient, notification.item.index, 'seen', at) || changed;
|
package/dist/notifications.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { planActNotifications, type WakeAdapter } from './delivery.js';
|
|
1
|
+
import { planActNotifications, type DeliveryModel, type WakeAdapter } from './delivery.js';
|
|
2
|
+
import { type SquareState } from './model.js';
|
|
2
3
|
import { matchesMentionTarget } from './runtime.js';
|
|
3
4
|
import { type ActivityId } from './square-core.js';
|
|
4
5
|
import type { WakeNotifier } from './square-facade.js';
|
|
@@ -26,5 +27,7 @@ export interface SweepPendingNotificationsOptions extends WorkerLaunchOptions {
|
|
|
26
27
|
now?: number;
|
|
27
28
|
limit?: number;
|
|
28
29
|
}
|
|
30
|
+
/** Select sweep candidates from one frozen snapshot and one delivery replay. */
|
|
31
|
+
export declare function pendingNotificationSweepFromState(squarePath: string, state: SquareState, now: number, env: NodeJS.ProcessEnv, limit: number, deriveDelivery?: (snapshot: SquareState) => DeliveryModel): number[];
|
|
29
32
|
/** Reconsider old pending attention at a bounded action boundary using the existing worker. */
|
|
30
33
|
export declare function sweepPendingNotifications(squarePath: string, opts?: SweepPendingNotificationsOptions): Promise<number[]>;
|
package/dist/notifications.js
CHANGED
|
@@ -2,7 +2,7 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { randomUUID } from 'node:crypto';
|
|
3
3
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { leaseOwnsNotification, planActNotifications, } from './delivery.js';
|
|
5
|
+
import { deriveDeliveryModel, leaseOwnsNotification, planActNotifications, } from './delivery.js';
|
|
6
6
|
import { sessionInbox } from './inbox.js';
|
|
7
7
|
import { hasPresentedAttention } from './presented.js';
|
|
8
8
|
import { SquareError } from './model.js';
|
|
@@ -256,6 +256,23 @@ export function wakeNotifierForSquare(squarePath, env = process.env) {
|
|
|
256
256
|
},
|
|
257
257
|
};
|
|
258
258
|
}
|
|
259
|
+
/** Select sweep candidates from one frozen snapshot and one delivery replay. */
|
|
260
|
+
export function pendingNotificationSweepFromState(squarePath, state, now, env, limit, deriveDelivery = deriveDeliveryModel) {
|
|
261
|
+
const delivery = deriveDelivery(state);
|
|
262
|
+
const pending = pendingDeliveriesFromState(state, delivery);
|
|
263
|
+
const evidence = wakeEvidenceProjectionFromState(squarePath, state, now, env, delivery);
|
|
264
|
+
const indexes = new Set();
|
|
265
|
+
for (const recipient of pending) {
|
|
266
|
+
for (const note of recipient.notifications) {
|
|
267
|
+
if (now - note.item.at <= wakeGraceMs(env))
|
|
268
|
+
continue;
|
|
269
|
+
if (!wakeIsEligible(evidence.evidence(recipient.recipient, note.item.index)))
|
|
270
|
+
continue;
|
|
271
|
+
indexes.add(note.item.index);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return [...indexes].sort((left, right) => left - right).slice(0, Math.max(0, limit));
|
|
275
|
+
}
|
|
259
276
|
/** Reconsider old pending attention at a bounded action boundary using the existing worker. */
|
|
260
277
|
export async function sweepPendingNotifications(squarePath, opts = {}) {
|
|
261
278
|
const env = opts.env ?? process.env;
|
|
@@ -271,19 +288,7 @@ export async function sweepPendingNotifications(squarePath, opts = {}) {
|
|
|
271
288
|
finally {
|
|
272
289
|
await closeOpenSquare(square);
|
|
273
290
|
}
|
|
274
|
-
const
|
|
275
|
-
const evidence = wakeEvidenceProjectionFromState(squarePath, state, now, env);
|
|
276
|
-
const indexes = new Set();
|
|
277
|
-
for (const delivery of pending) {
|
|
278
|
-
for (const note of delivery.notifications) {
|
|
279
|
-
if (now - note.item.at <= wakeGraceMs(env))
|
|
280
|
-
continue;
|
|
281
|
-
if (!wakeIsEligible(evidence.evidence(delivery.recipient, note.item.index)))
|
|
282
|
-
continue;
|
|
283
|
-
indexes.add(note.item.index);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
const selected = [...indexes].sort((a, b) => a - b).slice(0, Math.max(0, limit));
|
|
291
|
+
const selected = pendingNotificationSweepFromState(squarePath, state, now, env, limit);
|
|
287
292
|
const workerPath = fileURLToPath(new URL('./cmd/notify-once.js', import.meta.url));
|
|
288
293
|
for (const actIndex of selected) {
|
|
289
294
|
(opts.launchWorker ?? launchWorker)(workerPath, ['--location', squarePath, '--act-index', String(actIndex)]);
|
package/dist/presence.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { type DeliveryModel } from './delivery.js';
|
|
1
2
|
import type { OpenSquare } from './open-square.js';
|
|
2
3
|
import type { CatchOptions, CatchResult } from './square-facade.js';
|
|
3
|
-
export declare function catchUp(square: OpenSquare, name: string, options?: CatchOptions): Promise<CatchResult>;
|
|
4
|
+
export declare function catchUp(square: OpenSquare, name: string, options?: CatchOptions, deriveDelivery?: (state: import('./model.js').SquareState) => DeliveryModel): Promise<CatchResult>;
|
|
4
5
|
/** Commit seen only for complete, actually rendered boundary bodies. */
|
|
5
6
|
export declare function markBoundarySeen(squarePath: string, name: string, ownerId: string | undefined, actIndexes: readonly number[], at?: number): Promise<void>;
|
|
6
7
|
export declare function markNotificationNotified(square: OpenSquare, name: string, actIndex: number, ownerId: string | undefined, at?: number): Promise<void>;
|
package/dist/presence.js
CHANGED
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
import { extractMentions, formatActivityId } from './square-core.js';
|
|
2
2
|
import { deliveryDelta, directedPeerSays, matchesFeedFilter } from './activity-feed.js';
|
|
3
|
-
import {
|
|
3
|
+
import { deriveDeliveryModel, markSeenNotifications } from './delivery.js';
|
|
4
4
|
import { SquareError } from './model.js';
|
|
5
5
|
import { openSquare } from './square-file-adapter.js';
|
|
6
6
|
import { closeOpenSquare } from './open-square.js';
|
|
7
7
|
import { resolveKnownName } from './decisions.js';
|
|
8
|
-
import { readCursor } from './runtime.js';
|
|
9
8
|
import { recordObservation } from './runtime.js';
|
|
10
|
-
function expose(squareState, activity, viewer) {
|
|
9
|
+
function expose(squareState, activity, viewer, delivery) {
|
|
11
10
|
if (activity.kind === 'read' || activity.actor === undefined)
|
|
12
11
|
throw new Error(`Cannot expose stored activity ${formatActivityId(activity.index)}`);
|
|
13
|
-
const perception =
|
|
12
|
+
const perception = delivery.perceive(activity, viewer);
|
|
14
13
|
const result = { id: formatActivityId(activity.index), at: activity.at, kind: activity.kind, actor: activity.actor, mentions: activity.kind === 'say' ? extractMentions(activity.body) : [], ...('body' in activity && activity.body !== undefined ? { body: activity.body } : {}), ...('target' in activity ? { target: activity.target } : {}), ...(activity.kind === 'say' && activity.reply !== undefined ? { reply: formatActivityId(activity.reply) } : {}) };
|
|
15
14
|
if (perception === 'full' || !('body' in result))
|
|
16
15
|
return { ...result, perception };
|
|
17
16
|
const { body: _body, ...withoutBody } = result;
|
|
18
17
|
return { ...withoutBody, perception };
|
|
19
18
|
}
|
|
20
|
-
export async function catchUp(square, name, options = {}) {
|
|
19
|
+
export async function catchUp(square, name, options = {}, deriveDelivery = deriveDeliveryModel) {
|
|
21
20
|
const idle = options.idle ?? 0;
|
|
22
21
|
if (!Number.isFinite(idle) || idle < 0)
|
|
23
22
|
throw new SquareError('invalid_args', 'Catch idle duration must be a non-negative number');
|
|
@@ -26,15 +25,15 @@ export async function catchUp(square, name, options = {}) {
|
|
|
26
25
|
const attempt = await square.cell.transact((state, version) => {
|
|
27
26
|
const at = square.clock();
|
|
28
27
|
const viewer = resolveKnownName(state, name);
|
|
29
|
-
const
|
|
28
|
+
const delivery = deriveDelivery(state);
|
|
29
|
+
const delta = deliveryDelta(state, viewer, delivery);
|
|
30
30
|
const filter = { ...(options.from === undefined ? {} : { participants: [...options.from] }), ...(options.mention === true ? { mention: viewer } : {}) };
|
|
31
|
-
const delivered = directedPeerSays(state, delta, viewer).filter((activity) => matchesFeedFilter(activity, filter))
|
|
31
|
+
const delivered = directedPeerSays(state, delta, viewer, delivery).filter((activity) => matchesFeedFilter(activity, filter))
|
|
32
32
|
.filter((activity, index, activities) => activities.findIndex((candidate) => candidate.index === activity.index) === index)
|
|
33
33
|
.sort((left, right) => left.index - right.index);
|
|
34
|
-
const seenChanged = delivered.
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return { ...(seenChanged ? { state } : {}), result: { version, caught: { activities: delivered.map((activity) => expose(state, activity, viewer)), consumedThrough: cursor < 0 ? null : formatActivityId(cursor), idleExpired: false } } };
|
|
34
|
+
const seenChanged = delivered.length === 0 ? false : markSeenNotifications(state, viewer, delivered, at, delivery);
|
|
35
|
+
const cursor = delivery.cursorFor(viewer);
|
|
36
|
+
return { ...(seenChanged ? { state } : {}), result: { version, caught: { activities: delivered.map((activity) => expose(state, activity, viewer, delivery)), consumedThrough: cursor < 0 ? null : formatActivityId(cursor), idleExpired: false } } };
|
|
38
37
|
});
|
|
39
38
|
if (attempt.caught.activities.length > 0 || idle === 0)
|
|
40
39
|
return attempt.caught;
|
package/dist/presentation.d.ts
CHANGED
|
@@ -92,5 +92,6 @@ export declare function renderWatchOutput(history: StoredAct[], publicItems: Pub
|
|
|
92
92
|
mention?: string;
|
|
93
93
|
viewer: string;
|
|
94
94
|
showCatchHint?: boolean;
|
|
95
|
-
squareState
|
|
95
|
+
squareState?: SquareState;
|
|
96
|
+
perceptions?: ReadonlyMap<number, Perception>;
|
|
96
97
|
}): string;
|
package/dist/presentation.js
CHANGED
|
@@ -456,7 +456,8 @@ export function renderWatchOutput(history, publicItems, roomChanges, opts) {
|
|
|
456
456
|
.map((act) => renderAmbientEvent(act, opts.viewer, {
|
|
457
457
|
actNumber: act.kind === 'say' ? sayNumberFor(history, act) : undefined,
|
|
458
458
|
mention: opts.mention,
|
|
459
|
-
|
|
459
|
+
...(opts.perceptions?.has(act.index) ? { perception: opts.perceptions.get(act.index) } : {}),
|
|
460
|
+
...(opts.squareState === undefined ? {} : { squareState: opts.squareState }),
|
|
460
461
|
}))
|
|
461
462
|
.filter(Boolean)
|
|
462
463
|
.join('\n\n');
|
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,
|
|
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
|
|
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,
|
|
172
|
+
if (sameName(act.actor, recipient)) {
|
|
172
173
|
cursor = act.index;
|
|
173
174
|
continue;
|
|
174
175
|
}
|
|
175
|
-
if (!
|
|
176
|
+
if (!landed.includes(act, recipient)) {
|
|
176
177
|
cursor = act.index;
|
|
177
178
|
continue;
|
|
178
179
|
}
|
|
179
|
-
if (
|
|
180
|
+
if (squareState.runtime.observations?.[recipient]?.[formatActivityId(act.index)]?.state !== 'seen')
|
|
180
181
|
break;
|
|
181
182
|
cursor = act.index;
|
|
182
183
|
}
|
package/dist/square-core.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/square-core.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
137
|
+
function createFoldAccumulator() {
|
|
146
138
|
const ordered = [];
|
|
147
139
|
const byKey = new Map();
|
|
148
140
|
const hold = { active: false };
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
delete
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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);
|
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,7 +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): readonly PendingDeliveryProjection[];
|
|
99
|
+
export declare function pendingDeliveriesFromState(state: SquareState, delivery?: DeliveryModel): readonly PendingDeliveryProjection[];
|
|
100
100
|
export declare function pendingDeliveries(square: OpenSquare): Promise<readonly PendingDeliveryProjection[]>;
|
|
101
101
|
export declare function notificationEvidence(square: OpenSquare, recipient: string, actIndex: number): Promise<{
|
|
102
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
|
|
4
|
+
import { deriveDeliveryModel, isActivitySeen } from './delivery.js';
|
|
5
5
|
import { SquareError, nameKey } from './model.js';
|
|
6
|
-
import { countSays, currentHold, foldedState, freshWatchLease, inSquareCount, isCurrentlyJoined,
|
|
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(
|
|
13
|
-
const perception =
|
|
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
|
-
|
|
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
|
|
44
|
-
const activity = state.acts.findLast((candidate) => candidate.index <=
|
|
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,30 +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(
|
|
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' :
|
|
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
|
|
77
|
-
return { name, joined: false, notifications: [] }; const lease = freshWatchLease(state, known, square.clock()); return { name: known, joined: true, notifications:
|
|
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 =
|
|
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 ? [] :
|
|
81
|
-
export function pendingDeliveriesFromState(state) {
|
|
82
|
-
return [...new Set(state.acts.filter((activity) => activity.kind === 'join').map((activity) => activity.actor))]
|
|
83
|
-
.filter((name) => isCurrentlyJoined(state.acts, name))
|
|
84
|
-
.map((recipient) => ({ recipient, notifications: deriveDeliveryModel(state).pendingFor(recipient) }));
|
|
85
|
-
}
|
|
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
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(); return { delivered:
|
|
88
|
-
export async function notificationDelivered(square, recipient, actIndex) { const { state } = await square.cell.read(); return
|
|
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); }
|
package/dist/wake-evidence.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type WakeRoute } from './model.js';
|
|
2
|
+
import { type DeliveryModel } from './delivery.js';
|
|
2
3
|
import { type SquareState } from './model.js';
|
|
3
4
|
import { type WakeAttempt } from './wake-attempts.js';
|
|
4
5
|
export interface WakeEvidence {
|
|
@@ -14,7 +15,7 @@ export interface WakeEvidenceProjection {
|
|
|
14
15
|
}
|
|
15
16
|
/** Capture the primary wake facts once and derive any number of eligibility decisions from them. */
|
|
16
17
|
export declare function wakeEvidenceProjection(squarePath: string, now: number, env: NodeJS.ProcessEnv): Promise<WakeEvidenceProjection>;
|
|
17
|
-
export declare function wakeEvidenceProjectionFromState(squarePath: string, state: SquareState, now: number, env: NodeJS.ProcessEnv): WakeEvidenceProjection;
|
|
18
|
+
export declare function wakeEvidenceProjectionFromState(squarePath: string, state: SquareState, now: number, env: NodeJS.ProcessEnv, delivery?: DeliveryModel): WakeEvidenceProjection;
|
|
18
19
|
/** Project every wake decision from the same primary evidence. */
|
|
19
20
|
export declare function wakeEvidence(squarePath: string, recipient: string, actIndex: number, now: number, env: NodeJS.ProcessEnv): Promise<WakeEvidence>;
|
|
20
21
|
export declare function wakeIsEligible(evidence: WakeEvidence): boolean;
|
package/dist/wake-evidence.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { deriveDeliveryModel } from './delivery.js';
|
|
2
2
|
import { nameKey } from './model.js';
|
|
3
3
|
import { readPresentedAttentions } from './presented.js';
|
|
4
4
|
import { canonicalSquarePath, readActiveBindings } from './registry.js';
|
|
@@ -11,7 +11,7 @@ import { entryPresentation } from './views.js';
|
|
|
11
11
|
function attentionKey(squarePath, recipient, actIndex) {
|
|
12
12
|
return JSON.stringify([canonicalSquarePath(squarePath), nameKey(recipient), actIndex]);
|
|
13
13
|
}
|
|
14
|
-
function projectionFromState(squarePath, state, now, env) {
|
|
14
|
+
function projectionFromState(squarePath, state, now, env, delivery = deriveDeliveryModel(state)) {
|
|
15
15
|
const canonicalPath = canonicalSquarePath(squarePath);
|
|
16
16
|
const owners = new Map();
|
|
17
17
|
for (const binding of readActiveBindings(now)) {
|
|
@@ -52,7 +52,7 @@ function projectionFromState(squarePath, state, now, env) {
|
|
|
52
52
|
const presented = [...(presentedByAttention.get(key) ?? [])]
|
|
53
53
|
.some((ownerId) => recipientOwners.has(ownerId));
|
|
54
54
|
return {
|
|
55
|
-
delivered:
|
|
55
|
+
delivered: delivery.isSeen(recipient, actIndex),
|
|
56
56
|
notified: (() => {
|
|
57
57
|
const observation = observationFor(state, recipient, actIndex);
|
|
58
58
|
return observation?.state === 'notified'
|
|
@@ -80,8 +80,8 @@ export async function wakeEvidenceProjection(squarePath, now, env) {
|
|
|
80
80
|
await closeOpenSquare(square);
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
|
-
export function wakeEvidenceProjectionFromState(squarePath, state, now, env) {
|
|
84
|
-
return projectionFromState(squarePath, state, now, env);
|
|
83
|
+
export function wakeEvidenceProjectionFromState(squarePath, state, now, env, delivery) {
|
|
84
|
+
return projectionFromState(squarePath, state, now, env, delivery);
|
|
85
85
|
}
|
|
86
86
|
/** Project every wake decision from the same primary evidence. */
|
|
87
87
|
export async function wakeEvidence(squarePath, recipient, actIndex, now, env) {
|
package/dist/watch.d.ts
CHANGED
|
@@ -1,2 +1,3 @@
|
|
|
1
1
|
import { type WatchOptions } from './model.js';
|
|
2
|
-
|
|
2
|
+
/** `false` is reserved for a quiet --now; idle completion preserves its existing sweep boundary. */
|
|
3
|
+
export declare function cmdWatch(squarePath: string, name: string, opts: WatchOptions): Promise<boolean | undefined>;
|
package/dist/watch.js
CHANGED
|
@@ -19,10 +19,15 @@ function watchOutputResult(squarePath, presentation, name, caught, opts = {}) {
|
|
|
19
19
|
const delivered = caught.activities.flatMap((activity) => {
|
|
20
20
|
const index = parseActivityId(activity.id);
|
|
21
21
|
const stored = index === undefined ? undefined : presentation.activities.find((item) => item.index === index);
|
|
22
|
-
return stored === undefined ? [] : [stored];
|
|
22
|
+
return stored === undefined ? [] : [{ activity: stored, perception: activity.perception }];
|
|
23
23
|
});
|
|
24
|
-
const publicItems = delivered
|
|
25
|
-
|
|
24
|
+
const publicItems = delivered
|
|
25
|
+
.map(({ activity }) => activity)
|
|
26
|
+
.filter((item) => item.kind === 'say' || item.kind === 'done');
|
|
27
|
+
const roomChanges = delivered
|
|
28
|
+
.map(({ activity }) => activity)
|
|
29
|
+
.filter((item) => item.kind === 'join' || item.kind === 'done' || item.kind === 'hold' || item.kind === 'resume');
|
|
30
|
+
const perceptions = new Map(delivered.map(({ activity, perception }) => [activity.index, perception]));
|
|
26
31
|
return {
|
|
27
32
|
type: 'output',
|
|
28
33
|
stdout: renderWatchOutput([...presentation.activities], publicItems, roomChanges, {
|
|
@@ -30,7 +35,7 @@ function watchOutputResult(squarePath, presentation, name, caught, opts = {}) {
|
|
|
30
35
|
squarePath,
|
|
31
36
|
viewer: name,
|
|
32
37
|
showCatchHint: !hasAutomaticDeliveryIdentity(),
|
|
33
|
-
|
|
38
|
+
perceptions,
|
|
34
39
|
}),
|
|
35
40
|
...(opts.status ? { status: opts.status } : {}),
|
|
36
41
|
};
|
|
@@ -118,12 +123,14 @@ async function cmdWatchNow(squarePath, name, opts) {
|
|
|
118
123
|
? watchOutputResult(squarePath, presentation, name, caught, { mention: opts.mention, ...(status ? { status } : {}) })
|
|
119
124
|
: { type: 'terminal', status: status ?? 'empty-now' };
|
|
120
125
|
await finishWatchResult(square, squarePath, name, result, undefined);
|
|
126
|
+
return caught.activities.length > 0;
|
|
121
127
|
}
|
|
122
128
|
finally {
|
|
123
129
|
await facade.close();
|
|
124
130
|
await closeOpenSquare(square);
|
|
125
131
|
}
|
|
126
132
|
}
|
|
133
|
+
/** `false` is reserved for a quiet --now; idle completion preserves its existing sweep boundary. */
|
|
127
134
|
export async function cmdWatch(squarePath, name, opts) {
|
|
128
135
|
let square;
|
|
129
136
|
try {
|
|
@@ -144,8 +151,7 @@ export async function cmdWatch(squarePath, name, opts) {
|
|
|
144
151
|
}
|
|
145
152
|
if (opts.now) {
|
|
146
153
|
await closeOpenSquare(square);
|
|
147
|
-
|
|
148
|
-
return;
|
|
154
|
+
return cmdWatchNow(squarePath, name, opts);
|
|
149
155
|
}
|
|
150
156
|
const start = await beginWatch(square, squarePath, name, opts);
|
|
151
157
|
if (start.type === 'active') {
|
package/extensions/square-pi.js
CHANGED
|
@@ -44,6 +44,14 @@ export default function squarePiExtension(pi) {
|
|
|
44
44
|
});
|
|
45
45
|
};
|
|
46
46
|
|
|
47
|
+
// A transport may keep its promise pending while Pi is shutting down or
|
|
48
|
+
// replacing a session. Never make a lifecycle hook wait for that transport.
|
|
49
|
+
const stopWatcher = () => {
|
|
50
|
+
watcherAbort?.abort();
|
|
51
|
+
watcher = undefined;
|
|
52
|
+
watcherAbort = undefined;
|
|
53
|
+
};
|
|
54
|
+
|
|
47
55
|
const wake = async (piContext, token, signal) => {
|
|
48
56
|
while (sessionId !== undefined && token === generation && !signal.aborted) {
|
|
49
57
|
const deferredRetry = retryAfterChange;
|
|
@@ -91,10 +99,7 @@ export default function squarePiExtension(pi) {
|
|
|
91
99
|
|
|
92
100
|
pi.on('session_start', async (_event, ctx) => {
|
|
93
101
|
generation += 1;
|
|
94
|
-
|
|
95
|
-
if (watcher) await watcher;
|
|
96
|
-
watcher = undefined;
|
|
97
|
-
watcherAbort = undefined;
|
|
102
|
+
stopWatcher();
|
|
98
103
|
handledPending.clear();
|
|
99
104
|
retryAfterChange = false;
|
|
100
105
|
sessionId = ctx.sessionManager.getSessionId();
|
|
@@ -130,10 +135,7 @@ export default function squarePiExtension(pi) {
|
|
|
130
135
|
|
|
131
136
|
pi.on('session_shutdown', async () => {
|
|
132
137
|
generation += 1;
|
|
133
|
-
|
|
134
|
-
if (watcher) await watcher;
|
|
135
|
-
watcher = undefined;
|
|
136
|
-
watcherAbort = undefined;
|
|
138
|
+
stopWatcher();
|
|
137
139
|
for (const waiter of settledWaiters) waiter.resolve();
|
|
138
140
|
settledWaiters = [];
|
|
139
141
|
if (sessionId && sessionCwd) await automaticSessionEnd('pi', sessionId, sessionCwd);
|