@ccmsg/cli 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ccmsg/cli",
3
- "version": "0.5.0",
3
+ "version": "0.5.1",
4
4
  "description": "The ccmsg daemon, CLI and agent plugins for one instance (= one config home)",
5
5
  "license": "MIT",
6
6
  "author": "kawaz",
@@ -20,7 +20,7 @@
20
20
  "test": "bun test"
21
21
  },
22
22
  "dependencies": {
23
- "@ccmsg/protocol": "1.13.0"
23
+ "@ccmsg/protocol": "1.14.0"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/bun": "^1.3.0",
@@ -69,37 +69,69 @@ export function itemsRead(
69
69
  }
70
70
  const keep = selection(args.types === undefined ? {} : { types: args.types }, deps.presets);
71
71
  const { items } = select(within(classify(located(text)), args), keep);
72
- const page = paged(items, args.limit);
72
+ const page = paged(items, args.limit, backwards(args));
73
73
  return {
74
74
  items: page.items,
75
75
  ...(page.next === undefined ? {} : { next: page.next }),
76
+ ...(page.prev === undefined ? {} : { prev: page.prev }),
76
77
  ...(keep.keeps(IDS) ? { ids: ledger(page.items) } : {}),
77
78
  };
78
79
  }
79
80
 
81
+ /** Whether the range's end is the part to answer with.
82
+ *
83
+ * A caller that named where to start is reading forward from there; one that
84
+ * named only where to stop is looking at the newest of what it asked for, and
85
+ * answering with the oldest of that range would hand it the far side of a
86
+ * transcript it is walking back through. With neither bound the range is the
87
+ * whole transcript, which is read from its beginning. */
88
+ function backwards(bounds: TranscriptItemsReadArgs): boolean {
89
+ const lower =
90
+ bounds.since_at !== undefined ||
91
+ bounds.since_uuid !== undefined ||
92
+ bounds.since_id !== undefined;
93
+ const upper =
94
+ bounds.until_at !== undefined ||
95
+ bounds.until_uuid !== undefined ||
96
+ bounds.until_id !== undefined;
97
+ return upper && !lower;
98
+ }
99
+
80
100
  /** As much of the range as one answer carries, and where the next one starts.
81
101
  *
82
102
  * Two bounds, because either alone leaves a case unanswered: a count cannot
83
103
  * keep a page of long briefs inside what a connection should carry, and bytes
84
104
  * alone would answer with a number of items that varied with what was said in
85
- * them. Whichever is reached first ends the page, and the first item left out
86
- * is named so the caller resumes exactly where this stopped. */
105
+ * them. Whichever is reached first ends the page.
106
+ *
107
+ * Which end of the range is kept is the caller's, and the item named back is
108
+ * the one it continues from: reading forward, the first item left out, to be
109
+ * given as `since_id`; reading back, the first item answered, to be given as
110
+ * `until_id`. Either way the answer is oldest first, because that is the order
111
+ * a transcript has. */
87
112
  function paged(
88
113
  items: readonly Item[],
89
114
  limit: number | undefined,
90
- ): { items: Item[]; next?: string } {
115
+ back: boolean,
116
+ ): { items: Item[]; next?: string; prev?: string } {
91
117
  const most = Math.min(limit ?? ITEMS_LIMIT, ITEMS_LIMIT);
92
118
  const kept: Item[] = [];
93
119
  let held = 0;
94
- for (const item of items) {
120
+ for (let at = 0; at < items.length; at += 1) {
121
+ const item = items[back ? items.length - 1 - at : at];
122
+ if (item === undefined) continue;
95
123
  // A first item larger than the whole budget is still answered: a page of
96
124
  // nothing would leave the caller resuming at the item it just failed to
97
125
  // get, forever.
98
126
  if (kept.length > 0 && (kept.length >= most || held >= READ_LIMIT)) {
99
- return { items: kept, next: item.id };
127
+ if (!back) return { items: kept, next: item.id };
128
+ kept.reverse();
129
+ const first = kept[0];
130
+ return first === undefined ? { items: kept } : { items: kept, prev: first.id };
100
131
  }
101
132
  kept.push(item);
102
133
  held += JSON.stringify(item).length;
103
134
  }
135
+ if (back) kept.reverse();
104
136
  return { items: kept };
105
137
  }
@@ -149,13 +149,16 @@ export function select(
149
149
  * A record bound cuts at that record's position rather than at its clock, so
150
150
  * records sharing an instant stay on their own side of the cut — which is the
151
151
  * whole reason there are two kinds. An item bound is finer than either: it
152
- * resumes inside a record whose earlier items were already answered for. */
152
+ * cuts inside a record whose other items were already answered for, closed at
153
+ * the lower end where it resumes a read and open at the upper end where it
154
+ * stops short of what the caller already holds. */
153
155
  export interface Bounds {
154
156
  readonly since_at?: number;
155
157
  readonly since_uuid?: string;
156
158
  readonly since_id?: string;
157
159
  readonly until_at?: number;
158
160
  readonly until_uuid?: string;
161
+ readonly until_id?: string;
159
162
  }
160
163
 
161
164
  /** The bounds as stated, refused where they say two things at once.
@@ -170,8 +173,11 @@ export function bounded(bounds: Bounds): void {
170
173
  if (lower > 1) {
171
174
  throw new OpError("invalid_args", "a lower bound is a time, a record or an item, not several");
172
175
  }
173
- if (bounds.until_at !== undefined && bounds.until_uuid !== undefined) {
174
- throw new OpError("invalid_args", "an upper bound is a time or a record, not both");
176
+ const upper = [bounds.until_at, bounds.until_uuid, bounds.until_id].filter(
177
+ (one) => one !== undefined,
178
+ ).length;
179
+ if (upper > 1) {
180
+ throw new OpError("invalid_args", "an upper bound is a time, a record or an item, not several");
175
181
  }
176
182
  }
177
183
 
@@ -200,6 +206,9 @@ export function within(items: readonly Item[], bounds: Bounds): Item[] {
200
206
  }
201
207
  if (bounds.since_at !== undefined && item.at < bounds.since_at) continue;
202
208
  if (bounds.until_at !== undefined && item.at > bounds.until_at) break;
209
+ // An upper bound by item is open: it names an item the caller already
210
+ // holds, so the range ends before it rather than at it.
211
+ if (bounds.until_id !== undefined && item.id === bounds.until_id) break;
203
212
  kept.push(item);
204
213
  // An upper bound by record is inclusive and cuts after the last item that
205
214
  // record became, so the rest of the same record is still let through.