@goodandready/dsh-kanban 0.2.21 → 0.2.23

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/lib/board-tool.js CHANGED
@@ -59,7 +59,7 @@ function nameOf(task) {
59
59
  }
60
60
 
61
61
  /** board_move tool definition for `defineTool`. */
62
- export function boardMoveDefinition({ store, config }) {
62
+ export function boardMoveDefinition({ store, config, eventHub }) {
63
63
  return {
64
64
  name: 'board_move',
65
65
  description: 'Move the kanban card of the current task to another column. '
@@ -127,7 +127,7 @@ export function boardMoveDefinition({ store, config }) {
127
127
  /**
128
128
  * board_plan tool definition for `defineTool`.
129
129
  */
130
- export function boardPlanDefinition({ store }) {
130
+ export function boardPlanDefinition({ store, eventHub }) {
131
131
  return {
132
132
  name: 'board_plan',
133
133
  description: 'Publish or update the plan shown on the kanban card of the current task. '
@@ -157,7 +157,10 @@ export function boardPlanDefinition({ store }) {
157
157
  const task = tasks[0]
158
158
  const plan = applyPlan(parsePlan(task.plan), { items, current })
159
159
  if (plan.items.length === 0) return 'A plan needs at least one step.'
160
- for (const one of tasks) store.updateTask(one.id, { plan: serializePlan(plan) })
160
+ for (const one of tasks) {
161
+ store.updateTask(one.id, { plan: serializePlan(plan) })
162
+ eventHub?.broadcast?.('task:change', { taskId: one.id, action: 'updated' })
163
+ }
161
164
  const progress = planProgress(plan)
162
165
  return progress.text === ''
163
166
  ? `Plan of ${progress.total} steps recorded; no step is marked as current.`
@@ -168,7 +171,7 @@ export function boardPlanDefinition({ store }) {
168
171
 
169
172
 
170
173
  /** board_checklist tool definition (#205) */
171
- export function boardChecklistDefinition({ store }) {
174
+ export function boardChecklistDefinition({ store, eventHub }) {
172
175
  return {
173
176
  name: 'board_checklist',
174
177
  description: 'Mark a Definition of Done (DoD) checklist item as completed with verification evidence.',
@@ -429,7 +432,7 @@ export function boardDecomposeDefinition({ store, gitea }) {
429
432
  }
430
433
 
431
434
  /** board_checklist_item tool definition (#248) */
432
- export function boardChecklistItemDefinition({ store }) {
435
+ export function boardChecklistItemDefinition({ store, eventHub }) {
433
436
  return {
434
437
  name: 'board_checklist_item',
435
438
  description: 'Toggle or check a specific DoD checklist item by text or index without replacing the entire list.',
package/lib/client.js CHANGED
@@ -487,6 +487,7 @@ window.__ModuleLoader__.load({
487
487
  case 'cross-site': return 'error.crossSite'
488
488
  case 'model-not-selected': return 'error.modelNotSelected'
489
489
  case 'start-failed': return 'error.startFailed'
490
+ case 'timeout': return 'error.timeout'
490
491
  default: return 'error.unknown'
491
492
  }
492
493
  }
@@ -549,15 +550,54 @@ window.__ModuleLoader__.load({
549
550
  neighboursFor, tasksOf, sortTasks, packInfo, groupByAssignee, applyToEach, recallLaunch, rememberLaunch, stillOffered,
550
551
  cardRef, taskRef, cardStatus, facetLabel, boardTitle,
551
552
  columnTitle, planLine, stateLabel, isCollapsed,
552
- agoText, exactAt, shortDate, syncLine, readableOn, tagStyle, priorityColor, renderBlocks, renderSpans, matchesFilters, anySelected, toggleValue, normalizeBoard, errorKey,
553
+ agoText, exactAt, shortDate, syncLine, readableOn, tagStyle, priorityColor, renderBlocks, renderSpans, matchesFilters, anySelected, toggleValue, normalizeBoard, errorKey, api, createTimeoutSignal,
553
554
  chipSessionId, matchesQuery, splitFullName, repoOption, openSession, COLUMN_ORDER,
554
555
  }
555
556
 
556
557
 
558
+ function createTimeoutSignal(ms) {
559
+ const sig = typeof AbortSignal !== 'undefined' ? AbortSignal : (typeof globalThis !== 'undefined' ? globalThis.AbortSignal : undefined)
560
+ if (sig && typeof sig.timeout === 'function') {
561
+ return sig.timeout(ms)
562
+ }
563
+ const ctrl = typeof AbortController !== 'undefined' ? AbortController : (typeof globalThis !== 'undefined' ? globalThis.AbortController : undefined)
564
+ if (ctrl) {
565
+ const c = new ctrl()
566
+ setTimeout(() => c.abort(new Error('TimeoutError')), ms)
567
+ return c.signal
568
+ }
569
+ return undefined
570
+ }
571
+
557
572
  async function api(path, options) {
558
- const res = await fetch('/dsh-kanban' + path, Object.assign({
573
+ const timeoutMs = (options && options.timeout) || 20000
574
+ let timeoutSignal
575
+ try {
576
+ if (!options?.signal) {
577
+ timeoutSignal = createTimeoutSignal(timeoutMs)
578
+ }
579
+ } catch (_) {}
580
+
581
+ const fetchOpts = Object.assign({
559
582
  headers: { 'Content-Type': 'application/json' },
560
- }, options))
583
+ }, options)
584
+ if (timeoutSignal && !fetchOpts.signal) {
585
+ fetchOpts.signal = timeoutSignal
586
+ }
587
+
588
+ let res
589
+ try {
590
+ res = await fetch('/dsh-kanban' + path, fetchOpts)
591
+ } catch (err) {
592
+ if (err && (err.name === 'TimeoutError' || (!options?.signal && err.name === 'AbortError'))) {
593
+ const timeoutErr = new Error('timeout')
594
+ timeoutErr.key = 'error.timeout'
595
+ timeoutErr.name = 'TimeoutError'
596
+ throw timeoutErr
597
+ }
598
+ throw err
599
+ }
600
+
561
601
  let payload = null
562
602
  try { payload = await res.json() } catch { payload = null }
563
603
  if (!res.ok) {
@@ -1220,6 +1260,7 @@ window.__ModuleLoader__.load({
1220
1260
  'error.issueNotFound': 'Issue not found.',
1221
1261
  'error.titleRequired': 'A title is required.',
1222
1262
  'error.crossSite': 'Request rejected as cross-site.',
1263
+ 'error.timeout': 'Request timed out. Please try again.',
1223
1264
  'error.unknown': 'Request failed.',
1224
1265
  }
1225
1266
 
@@ -1611,6 +1652,7 @@ window.__ModuleLoader__.load({
1611
1652
  'error.issueNotFound': '没有找到 issue。',
1612
1653
  'error.titleRequired': '标题不能为空。',
1613
1654
  'error.crossSite': '请求被判定为跨站而拒绝。',
1655
+ 'error.timeout': '请求超时,请重试。',
1614
1656
  'error.unknown': '请求失败。',
1615
1657
  }
1616
1658
 
@@ -0,0 +1,582 @@
1
+ // License: CC0 (no rights reserved).
2
+
3
+ // This is based on https://observablehq.com/@dgreensp/implementing-fractional-indexing
4
+
5
+ export const BASE_62_DIGITS =
6
+ "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; // 0-9 + A-Z + a-z
7
+
8
+ export const BASE_52_DIGITS = BASE_62_DIGITS.slice(10); // A-Z + a-z
9
+
10
+ // `intDigits` (the integer-part head alphabet) defaults to `digits`. When
11
+ // `digits` is also omitted it falls back to BASE_52_DIGITS -- A-Z (negative
12
+ // lengths) followed by a-z (positive lengths) -- so the default keys keep the
13
+ // classic "a0", "Zz", ... form.
14
+
15
+ /**
16
+ * Per-alphabet cache mapping each digit's char code to its index, so digit->value
17
+ * lookups are a single typed-array load instead of O(alphabet) String.indexOf.
18
+ * Keys (and therefore alphabets) are required to be single-byte (char code
19
+ * 0-255), so a fixed 256-entry table covers every possible char code.
20
+ * @type {Map<string, Uint8Array>}
21
+ */
22
+ const digitIndexCache = new Map();
23
+
24
+ /**
25
+ * @param {string} digits
26
+ * @return {Uint8Array}
27
+ */
28
+ function getDigitIndex(digits) {
29
+ let m = digitIndexCache.get(digits);
30
+ if (m === undefined) {
31
+ m = new Uint8Array(256);
32
+ for (let i = 0; i < digits.length; i++) {
33
+ m[digits.charCodeAt(i)] = i;
34
+ }
35
+ digitIndexCache.set(digits, m);
36
+ }
37
+ return m;
38
+ }
39
+
40
+ // `a` may be empty string, `b` is null or non-empty string.
41
+ // `a < b` lexicographically if `b` is non-null.
42
+ // no trailing zeros allowed.
43
+ // digits is a string such as '0123456789' for base 10. Digits must be in
44
+ // ascending character code order!
45
+ /**
46
+ * @param {string} a
47
+ * @param {string | null | undefined} b
48
+ * @param {string} digits
49
+ * @param {Uint8Array} lookup
50
+ * @returns {string}
51
+ */
52
+ function midpoint(a, b, digits, lookup) {
53
+ const zero = digits[0];
54
+ if (b != null && a >= b) {
55
+ throw new Error(a + " >= " + b);
56
+ }
57
+ if (a.slice(-1) === zero || (b && b.slice(-1) === zero)) {
58
+ throw new Error("trailing zero");
59
+ }
60
+ if (b) {
61
+ // remove longest common prefix. pad `a` with 0s as we
62
+ // go. note that we don't need to pad `b`, because it can't
63
+ // end before `a` while traversing the common prefix.
64
+ let n = 0;
65
+ while ((a[n] || zero) === b[n]) {
66
+ n++;
67
+ }
68
+ if (n > 0) {
69
+ return b.slice(0, n) + midpoint(a.slice(n), b.slice(n), digits, lookup);
70
+ }
71
+ }
72
+ // first digits (or lack of digit) are different
73
+ const digitA = a ? /** @type {number} */ (lookup[a.charCodeAt(0)]) : 0;
74
+ const digitB =
75
+ b != null ? /** @type {number} */ (lookup[b.charCodeAt(0)]) : digits.length;
76
+ if (digitB - digitA > 1) {
77
+ const midDigit = Math.round(0.5 * (digitA + digitB));
78
+ return digits[midDigit];
79
+ } else {
80
+ // first digits are consecutive
81
+ if (b && b.length > 1) {
82
+ return b.slice(0, 1);
83
+ } else {
84
+ // `b` is null or has length 1 (a single digit).
85
+ // the first digit of `a` is the previous digit to `b`,
86
+ // or 9 if `b` is null.
87
+ // given, for example, midpoint('49', '5'), return
88
+ // '4' + midpoint('9', null), which will become
89
+ // '4' + '9' + midpoint('', null), which is '495'
90
+ return digits[digitA] + midpoint(a.slice(1), null, digits, lookup);
91
+ }
92
+ }
93
+ }
94
+
95
+ /**
96
+ * @param {string} int
97
+ * @param {string} intDigits
98
+ * @param {Uint8Array} intLookup
99
+ * @return {void}
100
+ */
101
+
102
+ function validateInteger(int, intDigits, intLookup) {
103
+ if (int.length !== getIntegerLength(int[0], intDigits, intLookup)) {
104
+ throw new Error("invalid integer part of order key: " + int);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * @param {string} head
110
+ * @param {string} intDigits
111
+ * @param {Uint8Array} intLookup
112
+ * @return {number}
113
+ */
114
+ function getIntegerLength(head, intDigits, intLookup) {
115
+ // intDigits is a single lexicographically ordered (ascending) alphabet: the
116
+ // first half are the negative-length heads and the second half the
117
+ // positive-length heads (the default A-Z/a-z markers are just one such
118
+ // alphabet). The outermost characters mark the longest integer parts, and the
119
+ // two heads straddling the midpoint mark the shortest (length 2).
120
+ const i = intLookup[head.charCodeAt(0)];
121
+ // `intLookup` returns 0 for any char code not in the alphabet, so confirm the
122
+ // char really is at index `i` before trusting it as a head.
123
+ if (intDigits[i] === head) {
124
+ const half = intDigits.length / 2;
125
+ return i < half ? half - i + 1 : i - half + 2;
126
+ }
127
+ throw new Error("invalid order key head: " + head);
128
+ }
129
+
130
+ /**
131
+ * @param {string} key
132
+ * @param {string} intDigits
133
+ * @param {Uint8Array} intLookup
134
+ * @return {string}
135
+ */
136
+
137
+ function getIntegerPart(key, intDigits, intLookup) {
138
+ const integerPartLength = getIntegerLength(key[0], intDigits, intLookup);
139
+ if (integerPartLength > key.length) {
140
+ throw new Error("invalid order key: " + key);
141
+ }
142
+ return key.slice(0, integerPartLength);
143
+ }
144
+
145
+ /**
146
+ * @param {string} key
147
+ * @param {string} digits
148
+ * @param {string} intDigits
149
+ * @param {Uint8Array} intLookup
150
+ * @return {void}
151
+ */
152
+ function validateOrderKey(key, digits, intDigits, intLookup) {
153
+ if (isSmallestInteger(key, digits, intDigits)) {
154
+ throw new Error("invalid order key: " + key);
155
+ }
156
+ // getIntegerPart will throw if the first character is bad,
157
+ // or the key is too short. we'd call it to check these things
158
+ // even if we didn't need the result
159
+ const i = getIntegerPart(key, intDigits, intLookup);
160
+ const f = key.slice(i.length);
161
+ if (f.slice(-1) === digits[0]) {
162
+ throw new Error("invalid order key: " + key);
163
+ }
164
+ }
165
+
166
+ // note that this may return null, as there is a largest integer
167
+ /**
168
+ * @param {string} x
169
+ * @param {string} digits
170
+ * @param {Uint8Array} lookup
171
+ * @param {string} intDigits
172
+ * @param {Uint8Array} intLookup
173
+ * @return {string | null}
174
+ */
175
+ function incrementInteger(x, digits, lookup, intDigits, intLookup) {
176
+ validateInteger(x, intDigits, intLookup);
177
+ const head = x[0];
178
+ const zero = digits[0];
179
+ // Walk the digit run right-to-left, turning maxed-out digits into zeros
180
+ // (`trailing`) until we find one we can bump.
181
+ let trailing = "";
182
+ for (let i = x.length - 1; i >= 1; i--) {
183
+ const d = /** @type {number} */ (lookup[x.charCodeAt(i)]) + 1;
184
+ if (d === digits.length) {
185
+ trailing = zero + trailing;
186
+ } else {
187
+ return head + x.slice(1, i) + digits[d] + trailing;
188
+ }
189
+ }
190
+ // carry out of the whole digit run; `trailing` is now all zeros.
191
+ const headIndex = intLookup[head.charCodeAt(0)];
192
+ if (headIndex === intDigits.length - 1) {
193
+ // already the largest integer
194
+ return null;
195
+ }
196
+ const h = intDigits[headIndex + 1];
197
+ // the head moves one step toward the largest digit; grow or shrink the digit
198
+ // run to match the new head's integer length.
199
+ const lengthDelta =
200
+ getIntegerLength(h, intDigits, intLookup) -
201
+ getIntegerLength(head, intDigits, intLookup);
202
+ return (
203
+ h +
204
+ (lengthDelta > 0
205
+ ? trailing + zero
206
+ : lengthDelta < 0
207
+ ? trailing.slice(1)
208
+ : trailing)
209
+ );
210
+ }
211
+
212
+ // note that this may return null, as there is a smallest integer
213
+ /**
214
+ * @param {string} x
215
+ * @param {string} digits
216
+ * @param {Uint8Array} lookup
217
+ * @param {string} intDigits
218
+ * @param {Uint8Array} intLookup
219
+ * @return {string | null}
220
+ */
221
+
222
+ function decrementInteger(x, digits, lookup, intDigits, intLookup) {
223
+ validateInteger(x, intDigits, intLookup);
224
+ const head = x[0];
225
+ const last = digits[digits.length - 1];
226
+ // Walk the digit run right-to-left, turning underflowed digits into the
227
+ // largest digit (`trailing`) until we find one we can drop.
228
+ let trailing = "";
229
+ for (let i = x.length - 1; i >= 1; i--) {
230
+ const d = /** @type {number} */ (lookup[x.charCodeAt(i)]) - 1;
231
+ if (d === -1) {
232
+ trailing = last + trailing;
233
+ } else {
234
+ return head + x.slice(1, i) + digits[d] + trailing;
235
+ }
236
+ }
237
+ // borrow out of the whole digit run; `trailing` is now all max digits.
238
+ const headIndex = intLookup[head.charCodeAt(0)];
239
+ if (headIndex === 0) {
240
+ // already the smallest integer
241
+ return null;
242
+ }
243
+ const h = intDigits[headIndex - 1];
244
+ // the head moves one step toward the smallest digit; grow or shrink the
245
+ // digit run to match the new head's integer length.
246
+ const lengthDelta =
247
+ getIntegerLength(h, intDigits, intLookup) -
248
+ getIntegerLength(head, intDigits, intLookup);
249
+ return (
250
+ h +
251
+ (lengthDelta > 0
252
+ ? trailing + last
253
+ : lengthDelta < 0
254
+ ? trailing.slice(1)
255
+ : trailing)
256
+ );
257
+ }
258
+
259
+ /**
260
+ * Two-level cache keyed by (intDigits, first digit char code). Nesting the maps
261
+ * lets us look up with the raw `intDigits` string and a primitive char code,
262
+ * avoiding the per-call allocation of a combined string key.
263
+ * @type {Map<string, Map<number, string>>}
264
+ */
265
+ const repeatedKeysCache = new Map();
266
+
267
+ /**
268
+ * @param {string} key
269
+ * @param {string} digits
270
+ * @param {string} intDigits
271
+ */
272
+ function isSmallestInteger(key, digits, intDigits) {
273
+ // The smallest integer is the most-negative head (the first character of
274
+ // intDigits, marking the longest integer part) followed by all-zero digits.
275
+ // Use a cache to avoid constructing the same long string over and over which
276
+ // causes unnecessary GC pressure.
277
+ let byDigit = repeatedKeysCache.get(intDigits);
278
+ if (byDigit === undefined) {
279
+ byDigit = new Map();
280
+ repeatedKeysCache.set(intDigits, byDigit);
281
+ }
282
+ const zeroCode = digits.charCodeAt(0);
283
+ let cached = byDigit.get(zeroCode);
284
+ if (cached === undefined) {
285
+ cached = intDigits[0] + digits[0].repeat(intDigits.length / 2);
286
+ byDigit.set(zeroCode, cached);
287
+ }
288
+ return key === cached;
289
+ }
290
+
291
+ /**
292
+ * Returns true if every character of `s` has a strictly greater character code
293
+ * than the one before it (ascending order, which also rules out duplicates).
294
+ * @param {string} s
295
+ * @return {boolean}
296
+ */
297
+ function isStrictlyAscending(s) {
298
+ for (let i = 1; i < s.length; i++) {
299
+ if (s.charCodeAt(i - 1) >= s.charCodeAt(i)) {
300
+ return false;
301
+ }
302
+ }
303
+ return true;
304
+ }
305
+
306
+ /**
307
+ * Returns true if every character of `s` is single-byte (char code < 256). Keys
308
+ * are required to be single-byte so digit lookups can use a fixed 256-entry
309
+ * table.
310
+ * @param {string} s
311
+ * @return {boolean}
312
+ */
313
+ function isSingleByte(s) {
314
+ for (let i = 0; i < s.length; i++) {
315
+ if (s.charCodeAt(i) > 255) {
316
+ return false;
317
+ }
318
+ }
319
+ return true;
320
+ }
321
+
322
+ /**
323
+ * Alphabets that have already passed `validateDigits`. Validation is pure and
324
+ * its result never changes, so we cache the alphabet to skip re-scanning it on
325
+ * every call. Kept separate from `validatedIntDigits` because the two have
326
+ * different rules (an odd-length string is a valid `digits` but not `intDigits`).
327
+ * @type {Set<string>}
328
+ */
329
+ const validatedDigits = new Set();
330
+
331
+ /**
332
+ * Validates a fractional-digit alphabet: at least two characters, in strictly
333
+ * ascending character-code order.
334
+ * @param {string} digits
335
+ * @return {void}
336
+ */
337
+ function validateDigits(digits) {
338
+ if (validatedDigits.has(digits)) {
339
+ return;
340
+ }
341
+ if (digits.length < 2 || !isStrictlyAscending(digits)) {
342
+ throw new Error(
343
+ "digits must be at least 2 characters in strictly ascending character " +
344
+ "code order: " +
345
+ digits
346
+ );
347
+ }
348
+ if (!isSingleByte(digits)) {
349
+ throw new Error("digits must be single-byte (char code 0-255): " + digits);
350
+ }
351
+ validatedDigits.add(digits);
352
+ }
353
+
354
+ /**
355
+ * Alphabets that have already passed `validateIntDigits`.
356
+ * @type {Set<string>}
357
+ */
358
+ const validatedIntDigits = new Set();
359
+
360
+ /**
361
+ * Validates a head-marker alphabet: an even number of at least two characters
362
+ * (its two halves are the negative- and positive-length heads), in strictly
363
+ * ascending character-code order.
364
+ * @param {string} intDigits
365
+ * @return {void}
366
+ */
367
+ function validateIntDigits(intDigits) {
368
+ if (validatedIntDigits.has(intDigits)) {
369
+ return;
370
+ }
371
+ if (
372
+ intDigits.length < 2 ||
373
+ intDigits.length % 2 !== 0 ||
374
+ !isStrictlyAscending(intDigits)
375
+ ) {
376
+ throw new Error(
377
+ "intDigits must be an even number of at least 2 characters in strictly " +
378
+ "ascending character code order: " +
379
+ intDigits
380
+ );
381
+ }
382
+ if (!isSingleByte(intDigits)) {
383
+ throw new Error(
384
+ "intDigits must be single-byte (char code 0-255): " + intDigits
385
+ );
386
+ }
387
+ validatedIntDigits.add(intDigits);
388
+ }
389
+
390
+ /**
391
+ * Generates an order key that sorts between `a` and `b`.
392
+ *
393
+ * `a` is the lower bound: an order key, or null for the start.
394
+ * `b` is the upper bound: an order key, or null for the end.
395
+ * When both are non-null, they may be passed in either order.
396
+ *
397
+ * `digits` is the alphabet, e.g. '0123456789' for base 10. Its characters
398
+ * must be single-byte (char code 0-255) and in ascending character code order;
399
+ * both are validated. It may otherwise be any alphabet (it does not need to
400
+ * contain 0-9, A-Z or a-z). Because `intDigits` defaults to `digits`, an
401
+ * odd-length `digits` must be paired with an explicit even-length `intDigits`.
402
+ *
403
+ * Note that `digits` only defines the *digit values* of a key. The integer
404
+ * part of every key also begins with a length/magnitude marker (a "head") drawn
405
+ * from the `intDigits` alphabet. The head only ever occupies the first position
406
+ * and is only compared against other heads, which is why `digits` and
407
+ * `intDigits` may overlap (or be identical) and keys still sort correctly.
408
+ *
409
+ * `intDigits` is the head alphabet: a single alphabet in ascending
410
+ * (lexicographical) character order, with even length. Its first half are the
411
+ * negative-length heads and its second half the positive-length heads. The
412
+ * outermost characters mark the longest integer parts and the two characters
413
+ * straddling the midpoint mark the shortest (length 2). The integer part may
414
+ * grow until it reaches the outermost heads, so a shorter alphabet limits how
415
+ * large/small a key's integer part can become.
416
+ *
417
+ * `intDigits` defaults to `digits`, so a base-10 alphabet produces self-headed
418
+ * keys like "50", "600" or "49". When `digits` is also omitted it falls back to
419
+ * BASE_52_DIGITS (A-Z/a-z), giving the classic "a0", "b00", "Z9" form. Note that
420
+ * passing `digits` explicitly (even BASE_62_DIGITS) makes the keys self-headed;
421
+ * only omitting `digits` entirely yields the A-Z/a-z heads.
422
+ *
423
+ * @example
424
+ * // base 10: heads come from `digits` itself. The first half (0-4) are negative
425
+ * // heads, the second half (5-9) positive heads, and 4/5 mark the shortest
426
+ * // (length-2) integer parts.
427
+ * generateKeyBetween(null, null, "0123456789"); // => "50"
428
+ *
429
+ * @param {string | null | undefined} a
430
+ * @param {string | null | undefined} b
431
+ * @param {string=} digits
432
+ * @param {string=} intDigits
433
+ * @return {string}
434
+ */
435
+ export function generateKeyBetween(
436
+ a,
437
+ b,
438
+ digits = undefined,
439
+ intDigits = undefined
440
+ ) {
441
+ if (intDigits !== undefined) {
442
+ validateIntDigits(intDigits);
443
+ } else {
444
+ intDigits = digits ?? BASE_52_DIGITS;
445
+ }
446
+ if (digits !== undefined) {
447
+ validateDigits(digits);
448
+ } else {
449
+ digits = BASE_62_DIGITS;
450
+ }
451
+
452
+ const lookup = getDigitIndex(digits);
453
+ const intLookup = getDigitIndex(intDigits);
454
+ if (a != null) {
455
+ validateOrderKey(a, digits, intDigits, intLookup);
456
+ }
457
+ if (b != null) {
458
+ validateOrderKey(b, digits, intDigits, intLookup);
459
+ }
460
+ if (a != null && b != null) {
461
+ // swap if out of order, so that a < b. this is just a convenience for
462
+ // callers, and doesn't affect the properties of the generated key.
463
+ if (a > b) {
464
+ const temp = a;
465
+ a = b;
466
+ b = temp;
467
+ }
468
+ }
469
+
470
+ if (a == null) {
471
+ if (b == null) {
472
+ // the shortest positive head: the first character of the second half of
473
+ // intDigits ("a" for the default A-Z/a-z markers).
474
+ const head = intDigits[intDigits.length / 2];
475
+ return head + digits[0];
476
+ }
477
+
478
+ const ib = getIntegerPart(b, intDigits, intLookup);
479
+ const fb = b.slice(ib.length);
480
+ if (isSmallestInteger(ib, digits, intDigits)) {
481
+ return ib + midpoint("", fb, digits, lookup);
482
+ }
483
+ if (ib < b) {
484
+ return ib;
485
+ }
486
+ const res = decrementInteger(ib, digits, lookup, intDigits, intLookup);
487
+ if (res == null) {
488
+ throw new Error("cannot decrement any more");
489
+ }
490
+ return res;
491
+ }
492
+
493
+ if (b == null) {
494
+ const ia = getIntegerPart(a, intDigits, intLookup);
495
+ const fa = a.slice(ia.length);
496
+ const i = incrementInteger(ia, digits, lookup, intDigits, intLookup);
497
+ return i == null ? ia + midpoint(fa, null, digits, lookup) : i;
498
+ }
499
+
500
+ const ia = getIntegerPart(a, intDigits, intLookup);
501
+ const fa = a.slice(ia.length);
502
+ const ib = getIntegerPart(b, intDigits, intLookup);
503
+ const fb = b.slice(ib.length);
504
+ if (ia === ib) {
505
+ return ia + midpoint(fa, fb, digits, lookup);
506
+ }
507
+ const i = incrementInteger(ia, digits, lookup, intDigits, intLookup);
508
+ if (i == null) {
509
+ throw new Error("cannot increment any more");
510
+ }
511
+ if (i < b) {
512
+ return i;
513
+ }
514
+ return ia + midpoint(fa, null, digits, lookup);
515
+ }
516
+
517
+ /**
518
+ * same preconditions as generateKeysBetween.
519
+ * n >= 0.
520
+ * Returns an array of n distinct keys in sorted order.
521
+ * If a and b are both null, returns [a0, a1, ...]
522
+ * If one or the other is null, returns consecutive "integer"
523
+ * keys. Otherwise, returns relatively short keys between
524
+ * a and b.
525
+ * @param {string | null | undefined} a
526
+ * @param {string | null | undefined} b
527
+ * @param {number} n
528
+ * @param {string=} digits
529
+ * @param {string=} intDigits
530
+ * @return {string[]}
531
+ */
532
+ export function generateNKeysBetween(
533
+ a,
534
+ b,
535
+ n,
536
+ digits = undefined,
537
+ intDigits = undefined
538
+ ) {
539
+ if (intDigits !== undefined) {
540
+ validateIntDigits(intDigits);
541
+ } else {
542
+ intDigits = digits ?? BASE_52_DIGITS;
543
+ }
544
+ if (digits !== undefined) {
545
+ validateDigits(digits);
546
+ } else {
547
+ digits = BASE_62_DIGITS;
548
+ }
549
+
550
+ if (n === 0) {
551
+ return [];
552
+ }
553
+ if (n === 1) {
554
+ return [generateKeyBetween(a, b, digits, intDigits)];
555
+ }
556
+ if (b == null) {
557
+ let c = generateKeyBetween(a, b, digits, intDigits);
558
+ const result = [c];
559
+ for (let i = 0; i < n - 1; i++) {
560
+ c = generateKeyBetween(c, b, digits, intDigits);
561
+ result.push(c);
562
+ }
563
+ return result;
564
+ }
565
+ if (a == null) {
566
+ let c = generateKeyBetween(a, b, digits, intDigits);
567
+ const result = [c];
568
+ for (let i = 0; i < n - 1; i++) {
569
+ c = generateKeyBetween(a, c, digits, intDigits);
570
+ result.push(c);
571
+ }
572
+ result.reverse();
573
+ return result;
574
+ }
575
+ const mid = Math.floor(n / 2);
576
+ const c = generateKeyBetween(a, b, digits, intDigits);
577
+ return [
578
+ ...generateNKeysBetween(a, c, mid, digits, intDigits),
579
+ c,
580
+ ...generateNKeysBetween(c, b, n - mid - 1, digits, intDigits),
581
+ ];
582
+ }