dimcode-darwin-x64 0.1.0-alpha.16 → 0.1.0-alpha.17

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.
Files changed (26) hide show
  1. package/bin/dimcode +2 -2
  2. package/default-plugins.json +0 -1
  3. package/package.json +1 -1
  4. package/plugins/scheduler/README.md +4 -9
  5. package/plugins/scheduler/data.ts +8 -29
  6. package/plugins/scheduler/settings.ts +0 -9
  7. package/plugins/workflow/README.md +22 -19
  8. package/plugins/workflow/store.ts +61 -18
  9. package/release-manifest.json +9 -26
  10. package/plugins/session-title/README.md +0 -76
  11. package/plugins/session-title/dim-plugin.json +0 -11
  12. package/plugins/session-title/index.ts +0 -13
  13. package/plugins/session-title/node_modules/.package-lock.json +0 -18
  14. package/plugins/session-title/node_modules/@arcships/rutis/README.md +0 -32
  15. package/plugins/session-title/node_modules/@arcships/rutis/package.json +0 -46
  16. package/plugins/session-title/node_modules/@arcships/rutis/src/advanced.d.ts +0 -90
  17. package/plugins/session-title/node_modules/@arcships/rutis/src/advanced.mjs +0 -32
  18. package/plugins/session-title/node_modules/@arcships/rutis/src/index.d.ts +0 -43
  19. package/plugins/session-title/node_modules/@arcships/rutis/src/index.mjs +0 -71
  20. package/plugins/session-title/node_modules/@arcships/rutis/src/testing.d.ts +0 -77
  21. package/plugins/session-title/node_modules/@arcships/rutis/src/testing.mjs +0 -350
  22. package/plugins/session-title/node_modules/@arcships/rutis/src/values.mjs +0 -261
  23. package/plugins/session-title/package-lock.json +0 -45
  24. package/plugins/session-title/package.json +0 -9
  25. package/plugins/session-title/part.ts +0 -172
  26. package/plugins/session-title/settings.ts +0 -49
@@ -1,350 +0,0 @@
1
- // Test a plugin on a real host: give it the services it injects, call the
2
- // services it provides, unload it.
3
- //
4
- // const t = await load(plugin, { config: { city: 'Oslo' }, services: { llm } })
5
- // assert.equal(await t.service('weather').today(), 'sunny in Oslo')
6
- // assert.equal(await t.serial('weather/ask', 'Oslo'), 'sunny')
7
- // await t.unload()
8
- //
9
- // Several plugins on one `host()`:
10
- //
11
- // const h = host({ services: { llm } })
12
- // const chat = await h.load(chatPlugin)
13
- // await h.parallel('session/created', id)
14
- //
15
- // The plugin runs in the real runtime (`@arcships/rutis-runtime`, or
16
- // `@arcships/rutis-bun` under Bun) of a real host (`rutis-host test`:
17
- // RUTIS_HOST, else the project's `@arcships/rutis-host`, else the one on
18
- // PATH), started once per test process (platform cleanup
19
- // §7.5): the host checks its config and fills its defaults, gates it on
20
- // the services it injects, and dispatches its events; what crosses between
21
- // it and the test crosses between processes. The test's listeners (`on`)
22
- // and what it emits are those of a plugin next to it in the same runtime,
23
- // the test tool's own; each of the test's services is a row of its own
24
- // there. The plugin is loaded from its
25
- // module, which must export it as default.
26
-
27
- import { createRequire } from 'node:module'
28
- import { join } from 'node:path'
29
- import { pathToFileURL } from 'node:url'
30
- import { PLUGIN_API, isPlugin } from './advanced.mjs'
31
-
32
- export class PluginTestError extends Error {
33
- name = 'PluginTestError'
34
- }
35
-
36
- const FILE = Symbol.for('rutis.plugin-file')
37
-
38
- // What the test serves the driver: its services' methods, by the
39
- // service's name, and its listeners, by `listener:<id>`.
40
- const served = new Map()
41
- let nextListener = 0
42
- function dispatch(target, method, args) {
43
- const object = served.get(target)
44
- if (object === undefined) throw new Error(`${target} is gone`)
45
- return target.startsWith('listener:') ? object(...args) : object[method](...args)
46
- }
47
-
48
- // `specifier` as the project in the current directory resolves it, as a
49
- // file URL, if it does.
50
- function fromProject(specifier) {
51
- try {
52
- return pathToFileURL(createRequire(join(process.cwd(), 'package.json')).resolve(specifier)).href
53
- } catch {
54
- return undefined
55
- }
56
- }
57
-
58
- // The host and runtime of this process, started at the first `load`.
59
- let started
60
- function runtime() {
61
- started ??= (async () => {
62
- const bun = typeof globalThis.Bun !== 'undefined'
63
- const name = bun ? '@arcships/rutis-bun' : '@arcships/rutis-runtime'
64
- // The project's runtime (a dev dependency of the project, not of this
65
- // package), else one this package can import.
66
- let start
67
- try {
68
- ({ start } = await import(fromProject(`${name}/testing`) ?? `${name}/testing`))
69
- } catch (error) {
70
- if (error?.code !== 'ERR_MODULE_NOT_FOUND' && error?.code !== 'MODULE_NOT_FOUND' && !/Cannot find (module|package)/.test(error?.message)) throw error
71
- throw new PluginTestError(`the test tool runs plugins in ${name}: add it as a dev dependency`)
72
- }
73
- const harness = await start({ dispatch, outgoing: hold })
74
- harness.control.unref()
75
- held = harness.control
76
- return harness.control
77
- })()
78
- return started
79
- }
80
-
81
- // `testEnded(fn)`: `fn` runs when the test running now ends, passed or
82
- // failed (a timeout too): node:test's `after`, under `node --test`, or
83
- // Bun's `onTestFinished`, under `bun test`. For the calls of a test
84
- // (hold) and the hosts it makes (host).
85
- let testEnded
86
- if (typeof globalThis.Bun !== 'undefined') {
87
- const { onTestFinished } = await import('bun:test')
88
- // Outside a test (a file's top level, `beforeAll`), Bun has no test to
89
- // end: nothing runs.
90
- testEnded = fn => { try { onTestFinished(fn) } catch {} }
91
- } else if (process.env.NODE_TEST_CONTEXT || process.execArgv.includes('--test')) {
92
- ({ after: testEnded } = await import('node:test'))
93
- }
94
-
95
- // The driver, synchronously (the test waits) or not.
96
- let control
97
- // While an asynchronous call of this process is out (a service's method,
98
- // a function or promise a plugin handed over), the session keeps the
99
- // process running, until its reply comes or the test that made it ends
100
- // (it failed at its timeout, say; testEnded): node:test reports and exits
101
- // once nothing keeps the process running.
102
- let held
103
- let out = 0
104
- function hold() {
105
- let holding = true
106
- if (out++ === 0) held?.ref()
107
- const release = () => {
108
- if (!holding) return
109
- holding = false
110
- if (--out === 0) held?.unref()
111
- }
112
- testEnded?.(release)
113
- return release
114
- }
115
- // `target`: the driver itself (''), or a service by its name.
116
- const NOT_STARTED = 'the host starts at the first load: load a plugin first'
117
- function call(method, ...args) {
118
- if (!control) throw new PluginTestError(NOT_STARTED)
119
- return control.call('', method, args)
120
- }
121
- function ask(method, ...args) {
122
- return askOf('', method, args)
123
- }
124
- async function askOf(target, method, args) {
125
- if (!control) throw new PluginTestError(NOT_STARTED)
126
- return control.callAsync(target, method, args)
127
- }
128
-
129
- // The methods rutis sees on a service no declaration describes: its own
130
- // and inherited functions, up to Object's, not the constructor.
131
- function methodNames(object) {
132
- const names = new Set()
133
- for (let level = object; level != null && level !== Object.prototype && level !== Function.prototype; level = Object.getPrototypeOf(level)) {
134
- for (const name of Object.getOwnPropertyNames(level)) {
135
- if (name !== 'constructor' && typeof Object.getOwnPropertyDescriptor(level, name)?.value === 'function') names.add(name)
136
- }
137
- }
138
- return [...names]
139
- }
140
-
141
- // The test's services `services`, provided next to the plugins; the
142
- // function that withdraws them.
143
- async function provide(services) {
144
- const names = []
145
- try {
146
- for (const [name, object] of Object.entries(services)) {
147
- served.set(name, object)
148
- names.push(name)
149
- await ask('provide', name, methodNames(object))
150
- }
151
- } catch (error) {
152
- await withdraw(names)
153
- throw error
154
- }
155
- return () => withdraw(names)
156
- }
157
- async function withdraw(names) {
158
- for (const name of names.reverse()) {
159
- served.delete(name)
160
- await ask('withdraw', name)
161
- }
162
- }
163
-
164
- // The five ways to emit and `filter`, from the test's plugin; `keep`, the
165
- // filter.
166
- function emitters(keep) {
167
- // `<mode>(name, ...values)`, or `filter.<mode>(keep, name, ...values)`.
168
- const method = mode => keep ? [`filter.${mode}`, keep] : [mode]
169
- const sync = mode => (...args) => call(...method(mode), ...args)
170
- const async = mode => (...args) => ask(...method(mode), ...args)
171
- return {
172
- emit: (...args) => { sync('emit')(...args) },
173
- parallel: async('parallel'),
174
- serial: async('serial'),
175
- waterfall: Object.assign(async('waterfall'), {
176
- with: terminal => {
177
- if (typeof terminal !== 'function') throw new PluginTestError('waterfall.with needs a terminal function')
178
- return (...args) => async('waterfall.with')(terminal, ...args)
179
- },
180
- }),
181
- bail: sync('bail'),
182
- filter: predicate => {
183
- if (typeof predicate !== 'function') throw new PluginTestError('filter needs a predicate function')
184
- const { filter: _, ...five } = emitters(predicate)
185
- return five
186
- },
187
- }
188
- }
189
-
190
- // The module that exports `plugin` as default, as a real runtime loads it.
191
- async function fileOf(plugin) {
192
- const file = plugin[FILE]
193
- let module
194
- try {
195
- module = file && await import(file)
196
- } catch {}
197
- if (module?.default !== plugin) {
198
- throw new PluginTestError(`load needs a plugin that its module exports as default${file ? `: ${file} does not` : ''}`)
199
- }
200
- return file
201
- }
202
-
203
- /**
204
- * Plugins on the host, sharing its bus. `services` are the test's, given
205
- * to every plugin that injects them. Not unloaded when the test that made
206
- * it ends (it failed before `unload`, say), it unloads then: the next test
207
- * finds none of its rows or services.
208
- */
209
- export function host({ services = {} } = {}) {
210
- const rows = []
211
- let withdrawShared
212
- let closed = false
213
- const self = {
214
- /** The failures the host reported of the rows here (§5.6): `{ line }`. */
215
- async reported() { return (await Promise.all(rows.map(row => row.api.reported()))).flat() },
216
- /**
217
- * Load `plugin` as a row, with `config` and its own `services`; `id`,
218
- * the row's id; `isolate`, as a row's (`{ forecast: true }` or a label).
219
- */
220
- async load(plugin, { config, services: own = {}, id: rowId, isolate } = {}) {
221
- if (closed) throw new PluginTestError('the host is unloaded')
222
- plugin = isPlugin(plugin) ? plugin : plugin?.default
223
- if (!isPlugin(plugin)) throw new PluginTestError('load needs a plugin made with definePlugin')
224
- if (plugin.api > PLUGIN_API) {
225
- throw new PluginTestError(`the plugin needs plugin API ${plugin.api}; this SDK supports ${PLUGIN_API}`)
226
- }
227
- const file = await fileOf(plugin)
228
- control = await runtime()
229
- withdrawShared ??= await provide(services)
230
- const withdrawOwn = await provide(own)
231
- let id
232
- try {
233
- id = await ask('load', file, config ?? null, { ...(rowId === undefined ? {} : { id: rowId }), ...(isolate === undefined ? {} : { isolate }) })
234
- } catch (error) {
235
- await withdrawOwn()
236
- throw new PluginTestError(error?.message ?? String(error))
237
- }
238
- const row = loaded(id, plugin, withdrawOwn)
239
- rows.push(row)
240
- return row.api
241
- },
242
- /** Unload every row, latest first. */
243
- async unload() {
244
- closed = true
245
- for (const row of [...rows].reverse()) await row.unload()
246
- await withdrawShared?.()
247
- withdrawShared = undefined
248
- },
249
- ...emitters(undefined),
250
- }
251
- testEnded?.(() => self.unload())
252
-
253
- function loaded(id, plugin, withdrawOwn) {
254
- // This row's test listeners: withdrawn when it unloads.
255
- const mine = new Set()
256
- let unloaded = false
257
- const shape = name => {
258
- if (!call('provided', id).includes(name)) throw new PluginTestError(`the plugin does not provide ${name} (now)`)
259
- return plugin.provides[name] ?? call('methods', name)
260
- }
261
- // Unload the row as the host does, then withdraw its services and the
262
- // test's listeners of it.
263
- const unload = async () => {
264
- if (unloaded) return
265
- unloaded = true
266
- rows.splice(rows.indexOf(record), 1)
267
- await ask('unload', id)
268
- for (const off of [...mine]) off()
269
- await withdrawOwn()
270
- }
271
- const api = {
272
- /** The row's id on the host. */
273
- row: id,
274
- /** The service `name` the plugin provides, as rutis sees it: its declared methods, or, undeclared, the object's methods, all async. */
275
- service(name) {
276
- if (unloaded) throw new PluginTestError('the plugin is unloaded')
277
- return Object.fromEntries(Object.entries(shape(name)).map(([method, kind]) => [method, kind === 'sync'
278
- ? (...args) => control.call(name, method, args)
279
- : (...args) => askOf(name, method, args)]))
280
- },
281
- /** The methods rutis sees on the service `name`: `{ method: 'sync' | 'async' }`. */
282
- methods: name => ({ ...shape(name) }),
283
- /** The names of the services the plugin provides now. */
284
- provided: () => call('provided', id),
285
- /**
286
- * The failures the host reported of this row (a listener failing in an
287
- * `emit`, a filter): `{ line }`, `line` as the host prints it.
288
- */
289
- async reported() { return unloaded ? [] : (await ask('reported', id)).map(line => ({ line })) },
290
- /** The host this row is loaded on. */
291
- host: self,
292
- ...emitters(undefined),
293
- /**
294
- * Listen to the event `name` as a plugin next to it does, until the
295
- * returned function is called or the row unloads. Options as
296
- * `ctx.on`: `prepend`, `global`.
297
- */
298
- on(name, listener, options) {
299
- if (unloaded) throw new PluginTestError('the plugin is unloaded')
300
- if (typeof listener !== 'function') throw new PluginTestError('on needs a listener function')
301
- const target = `listener:${++nextListener}`
302
- served.set(target, listener)
303
- try {
304
- call('on', target, name, { prepend: Boolean(options?.prepend), global: Boolean(options?.global) })
305
- } catch (error) {
306
- served.delete(target)
307
- throw error
308
- }
309
- const off = () => {
310
- if (!mine.delete(off)) return
311
- served.delete(target)
312
- call('off', target)
313
- }
314
- mine.add(off)
315
- return off
316
- },
317
- /** Change the row's config, as a host does. */
318
- async update(config) {
319
- if (unloaded) throw new PluginTestError('the plugin is unloaded')
320
- try {
321
- await ask('update', id, config ?? null)
322
- } catch (error) {
323
- throw new PluginTestError(error?.message ?? String(error))
324
- }
325
- },
326
- /** Unload the row as the host does, then withdraw its services and the test's listeners of it. */
327
- unload,
328
- }
329
- const record = { api, unload }
330
- return record
331
- }
332
-
333
- return self
334
- }
335
-
336
- /**
337
- * Load `plugin` (what definePlugin returns, or a module whose default export
338
- * it is) with `config` and the services in `services`, on a `host()` of its
339
- * own.
340
- */
341
- export async function load(plugin, { config, services = {} } = {}) {
342
- const h = host({ services })
343
- const t = await h.load(plugin, { config }).catch(async error => {
344
- await h.unload()
345
- throw error
346
- })
347
- // Unloading it unloads its host too, with the test's services.
348
- t.unload = () => h.unload()
349
- return t
350
- }
@@ -1,261 +0,0 @@
1
- // The tagged values beyond JSON data (#308, #355, #360): a point in time,
2
- // a local date and time, a date, a time of day, binary data, a map, a set,
3
- // a big integer, a decimal and a UUID. Each crosses a session under its own
4
- // tag, as text in the formats below, and only to a far end that declared
5
- // the capability `value-<tag>`.
6
- //
7
- // | tag | JavaScript sends | receives | text on the wire |
8
- // | ---------- | ----------------------- | ------------ | ------------------------------------ |
9
- // | `instant` | `Date` | `Date` | `2026-10-15T03:22:15.123Z`, UTC |
10
- // | `datetime` | `rutis.datetime(text)` | string | `2026-10-15T14:30:05.25`, no zone |
11
- // | `date` | `rutis.date(text)` | string | `2026-10-15` |
12
- // | `time` | `rutis.time(text)` | string | `14:30:05.25`, 24-hour, no zone |
13
- // | `bytes` | `Uint8Array` (`Buffer`), `ArrayBuffer`, `DataView` | `Uint8Array` | base64, padded |
14
- // | `map` | `Map` | `Map` | `[[key, value], …]`, in order |
15
- // | `set` | `Set` | `Set` | `[element, …]`, in order |
16
- // | `bigint` | `bigint` | `bigint` | decimal integer, at most 4300 digits |
17
- // | `decimal` | `rutis.decimal(text)` | string | `-12.3400`, trailing zeros kept |
18
- // | `uuid` | `rutis.uuid(text)` | string | lowercase `8-4-4-4-12` hex |
19
- //
20
- // Years are 0001–9999; fractions of a second have 0–9 digits; an instant is
21
- // truncated to the millisecond toward negative infinity when received. The
22
- // constructors at the end make objects marked with
23
- // `Symbol.for('rutis.value')`, so the runtimes know them without importing
24
- // this package; the testing tool crosses values by the same rules. This is
25
- // the SDK's own copy of the runtimes' rules (node/rutis-runtime/src/
26
- // values.mjs). Design: docs/design-value-types-2026-10-15.md; the
27
- // normative wire text is in docs/design-protocol-plugins-2026-09-25.md
28
- // §6.1; the Rust reference is crates/rutis-bridge/src/session/values.rs.
29
-
30
- // The mark on what the SDK constructors make: `{ [MARK]: tag, value: text }`.
31
- export const MARK = Symbol.for('rutis.value')
32
- // The tags the SDK constructors make; the rest are JavaScript values.
33
- const CONSTRUCTED = ['datetime', 'date', 'time', 'decimal', 'uuid']
34
- // The most digits a bigint or decimal may have: Python's default limit on
35
- // integer text.
36
- export const MAX_DIGITS = 4300
37
-
38
- // A value this side cannot send or take: only the call carrying it fails,
39
- // the session goes on (unlike a frame not in the protocol's shape).
40
- export class ValueError extends TypeError {}
41
-
42
- // A text a value error quotes, shortened to 40 characters.
43
- const quoted = text => text.length > 40 ? `${JSON.stringify(text.slice(0, 40))}…` : JSON.stringify(text)
44
- const invalid = (what, text, expected) => new ValueError(`invalid ${what} ${quoted(text)}: expected ${expected}`)
45
-
46
- // ── The text formats ──────────────────────────────────────────────────
47
-
48
- const DATE = /^(\d{4})-(\d{2})-(\d{2})$/
49
- const TIME = /^(\d{2}):(\d{2}):(\d{2})(?:\.(\d{1,9}))?$/
50
-
51
- const leap = year => year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0)
52
- const daysInMonth = (year, month) => month === 2 ? (leap(year) ? 29 : 28) : [4, 6, 9, 11].includes(month) ? 30 : 31
53
-
54
- // `[year, month, day]` of `YYYY-MM-DD`, a day of years 0001–9999.
55
- function readDate(text) {
56
- const match = DATE.exec(text)
57
- if (!match) return undefined
58
- const [year, month, day] = match.slice(1).map(Number)
59
- if (year < 1 || month < 1 || month > 12 || day < 1 || day > daysInMonth(year, month)) return undefined
60
- return [year, month, day]
61
- }
62
- // `[hour, minute, second, fraction digits]` of `HH:MM:SS[.f…]`.
63
- function readTime(text) {
64
- const match = TIME.exec(text)
65
- if (!match) return undefined
66
- const [hour, minute, second] = match.slice(1, 4).map(Number)
67
- if (hour > 23 || minute > 59 || second > 59) return undefined
68
- return [hour, minute, second, match[4] ?? '']
69
- }
70
- // `[date, time]` of `YYYY-MM-DDTHH:MM:SS[.f…]`.
71
- function readDateTime(text) {
72
- const at = text.indexOf('T')
73
- if (at !== 10) return undefined
74
- const date = readDate(text.slice(0, 10)), time = readTime(text.slice(11))
75
- return date && time ? [date, time] : undefined
76
- }
77
-
78
- // Days from 1970-01-01 to `year-month-day` (H. Hinnant's `days_from_civil`).
79
- function daysFromCivil(year, month, day) {
80
- year -= month <= 2 ? 1 : 0
81
- const era = Math.floor(year / 400)
82
- const yearOfEra = year - era * 400
83
- const dayOfYear = Math.floor((153 * ((month + 9) % 12) + 2) / 5) + day - 1
84
- const dayOfEra = yearOfEra * 365 + Math.floor(yearOfEra / 4) - Math.floor(yearOfEra / 100) + dayOfYear
85
- return era * 146097 + dayOfEra - 719468
86
- }
87
-
88
- // 0001-01-01T00:00:00Z and 9999-12-31T23:59:59.999Z, in milliseconds.
89
- const FIRST_MS = -62135596800000
90
- const LAST_MS = 253402300799999
91
-
92
- // Checks of each text, as the wire takes it.
93
- const formats = {
94
- instant: text => text.endsWith('Z') && readDateTime(text.slice(0, -1)) ? undefined : 'RFC 3339 in UTC, YYYY-MM-DDTHH:MM:SS[.fraction]Z, years 0001–9999',
95
- datetime: text => readDateTime(text) ? undefined : 'YYYY-MM-DDTHH:MM:SS[.fraction] without a time zone, years 0001–9999',
96
- date: text => readDate(text) ? undefined : 'YYYY-MM-DD, years 0001–9999',
97
- time: text => readTime(text) ? undefined : 'HH:MM:SS[.fraction], 24-hour, 0–9 fraction digits',
98
- bigint: text => /^-?(0|[1-9]\d*)$/.test(text) && text !== '-0' && text.replace('-', '').length <= MAX_DIGITS ? undefined : `a decimal integer without leading zeros, at most ${MAX_DIGITS} digits`,
99
- decimal: text => /^-?(0|[1-9]\d*)(\.\d+)?$/.test(text) && text.replace(/[-.]/g, '').length <= MAX_DIGITS ? undefined : `decimal digits with an optional fraction, no exponent, at most ${MAX_DIGITS} digits`,
100
- uuid: text => /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(text) ? undefined : 'lowercase hex 8-4-4-4-12',
101
- }
102
-
103
- // `text`, if it is a valid `tag` value; else a ValueError.
104
- export function checkText(tag, text) {
105
- if (typeof text !== 'string') throw new ValueError(`invalid ${tag}: expected its text, not ${describe(text)}`)
106
- const expected = formats[tag](text)
107
- if (expected) throw invalid(tag, text, expected)
108
- return text
109
- }
110
-
111
- // The Date an instant's text names, truncated to the millisecond toward
112
- // negative infinity (design V7).
113
- export function instantFromText(text) {
114
- checkText('instant', text)
115
- const [[year, month, day], [hour, minute, second, fraction]] = readDateTime(text.slice(0, -1))
116
- const seconds = daysFromCivil(year, month, day) * 86400 + hour * 3600 + minute * 60 + second
117
- return new Date(seconds * 1000 + Number(fraction.padEnd(3, '0').slice(0, 3)))
118
- }
119
-
120
- // A Date's text as an instant; a ValueError for an invalid Date or one
121
- // outside years 0001–9999.
122
- export function instantText(date) {
123
- const time = date.getTime()
124
- if (Number.isNaN(time)) throw new ValueError('cannot send an invalid Date')
125
- if (time < FIRST_MS || time > LAST_MS) throw new ValueError(`cannot send the Date ${date.toISOString()}: an instant is within years 0001–9999`)
126
- // `YYYY-MM-DDTHH:MM:SS.sssZ`, without trailing zeros in the fraction.
127
- return date.toISOString().replace(/\.?0*Z$/, 'Z')
128
- }
129
-
130
- // ── Binary data ──────────────────────────────────────────────────────
131
-
132
- // The bytes of a Uint8Array, ArrayBuffer or DataView (a Buffer is a
133
- // Uint8Array); undefined for anything else.
134
- export function bytesOf(value) {
135
- if (value instanceof Uint8Array) return value
136
- if (value instanceof ArrayBuffer) return new Uint8Array(value)
137
- if (value instanceof DataView) return new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
138
- return undefined
139
- }
140
-
141
- // ── What crosses as a tagged value ───────────────────────────────────
142
-
143
- // The tag of an SDK constructor's value, if `value` is one.
144
- export const constructedTag = value => value !== null && typeof value === 'object' && CONSTRUCTED.includes(value[MARK]) ? value[MARK] : undefined
145
-
146
- // The tag `value` crosses under, if it crosses as a tagged value. Other
147
- // typed arrays (Int16Array, ...) are not bytes: their element type would
148
- // be lost.
149
- export function tagOf(value) {
150
- if (typeof value === 'bigint') return 'bigint'
151
- if (value === null || typeof value !== 'object') return undefined
152
- if (value instanceof Date) return 'instant'
153
- if (value instanceof Map) return 'map'
154
- if (value instanceof Set) return 'set'
155
- if (bytesOf(value)) return 'bytes'
156
- return constructedTag(value)
157
- }
158
-
159
- // What `value` is, for errors.
160
- export function describe(value) {
161
- if (value === null) return 'null'
162
- if (value === undefined) return 'undefined'
163
- if (Array.isArray(value)) return 'an array'
164
- const tag = tagOf(value)
165
- if (tag) return { instant: 'a Date', bytes: 'binary data', map: 'a Map', set: 'a Set', bigint: 'a bigint' }[tag] ?? `a ${tag}`
166
- if (ArrayBuffer.isView(value)) return `a ${value.constructor.name}`
167
- if (typeof value === 'object') return 'an object'
168
- return `a ${typeof value}`
169
- }
170
-
171
- // ── Map keys and set elements (design §5) ────────────────────────────
172
-
173
- // The fraction of a time without trailing zeros: `14:30:05.250` and
174
- // `14:30:05.25` are one time.
175
- const shortTime = text => text.replace(/(\.\d*?)0+$/, '$1').replace(/\.$/, '')
176
-
177
- const unsafeKey = (what, number) => new ValueError(`a ${what} cannot be the integer ${number}, beyond ±(2^53-1): send it as a bigint`)
178
- const refusedKey = (what, kind) => new ValueError(`a ${what} cannot be ${kind}: only strings, finite numbers, booleans, bigint, datetime, date, time, decimal and uuid values can (an instant, bytes, null, an array, an object, a map, a set or a reference cannot)`)
179
-
180
- // How a key compares with the others of its map (`what` "map key") or set
181
- // ("set element"), as `tag` and its JavaScript value: numbers by value (0
182
- // and -0 are one), the rest by kind and text. A ValueError for a value
183
- // that is not a key in every language.
184
- export function keyIdentity(tag, value, what) {
185
- switch (tag) {
186
- case 'data':
187
- if (typeof value === 'string') return `s${value}`
188
- if (typeof value === 'boolean') return `b${value}`
189
- if (typeof value === 'number') {
190
- if (!Number.isFinite(value)) throw refusedKey(what, String(value))
191
- // Beyond ±(2^53−1) two integers may be one double: send a bigint.
192
- if (Number.isInteger(value) && !Number.isSafeInteger(value)) throw unsafeKey(what, value)
193
- return `n${value === 0 ? 0 : value}`
194
- }
195
- throw refusedKey(what, describe(value))
196
- case 'bigint': return `bigint${value}`
197
- case 'datetime': case 'time': return `${tag}${shortTime(value)}`
198
- case 'date': case 'decimal': case 'uuid': return `${tag}${value}`
199
- default: throw refusedKey(what, tag === 'instant' ? 'an instant' : describe(value))
200
- }
201
- }
202
-
203
- // A key or element this side sends, as `[tag, value]` for keyIdentity:
204
- // an SDK constructor's value as its tag and checked text.
205
- export function keyOf(value) {
206
- const constructed = constructedTag(value)
207
- if (constructed) return [constructed, checkText(constructed, value.value)]
208
- if (value === null || typeof value === 'object' || typeof value === 'function' || value === undefined) return [tagOf(value) ?? 'other', value]
209
- return [typeof value === 'bigint' ? 'bigint' : 'data', value]
210
- }
211
-
212
- // Each `[tag, value]` of `keys` a key, no two equal (a language that merged
213
- // them would lose an entry); then none equal once received in JavaScript
214
- // (`received`, the Map or Set made), where a date, decimal or ... is its
215
- // text and may meet a string key.
216
- export function checkKeys(keys, what, received) {
217
- const seen = new Set()
218
- for (const [tag, value] of keys) {
219
- const identity = keyIdentity(tag, value, what)
220
- if (seen.has(identity)) throw new ValueError(`duplicate ${what} ${shownKey(value)}: a map or set whose keys collide would lose entries`)
221
- seen.add(identity)
222
- }
223
- if (received && received.size !== seen.size) {
224
- throw new ValueError(`duplicate ${what} once received: two ${what}s are one key in JavaScript (a datetime, date, time, decimal or uuid arrives as its text), so an entry would be lost`)
225
- }
226
- }
227
-
228
- function shownKey(value) {
229
- const text = typeof value === 'string' ? JSON.stringify(value) : String(value)
230
- return text.length > 40 ? `${text.slice(0, 40)}…` : text
231
- }
232
-
233
- // ── The constructors ─────────────────────────────────────────────────
234
-
235
- // A local date and time, date, time of day, decimal or UUID: JavaScript
236
- // has no type for them, so a plugin receives them as strings and sends
237
- // them as one of these. A string stays a string.
238
- class Value {
239
- constructor(tag, text) {
240
- this[MARK] = tag
241
- this.value = text
242
- Object.freeze(this)
243
- }
244
- toString() { return this.value }
245
- toJSON() { return this.value }
246
- }
247
-
248
- const make = (tag, text) => new Value(tag, checkText(tag, text))
249
- // `HH:MM` as `HH:MM:00`: the wire always has seconds.
250
- const withSeconds = text => typeof text === 'string' ? text.replace(/(^|T)(\d{2}:\d{2})$/, '$1$2:00') : text
251
-
252
- /** A local date and time without a time zone (`datetime`): `'2026-10-15T14:30'`, `'2026-10-15T14:30:05.25'`. */
253
- export const datetime = text => make('datetime', withSeconds(text))
254
- /** A calendar date (`date`): `'2026-10-15'`. */
255
- export const date = text => make('date', text)
256
- /** A time of day without a time zone (`time`): `'14:30'`, `'14:30:05.25'`. */
257
- export const time = text => make('time', withSeconds(text))
258
- /** A decimal number (`decimal`), its trailing zeros kept: `'1.10'`, `'-12.3400'`; a bigint is taken as its digits. */
259
- export const decimal = text => make('decimal', typeof text === 'bigint' ? String(text) : text)
260
- /** A UUID (`uuid`), in either case; sent in lowercase. */
261
- export const uuid = text => make('uuid', typeof text === 'string' ? text.toLowerCase() : text)
@@ -1,45 +0,0 @@
1
- {
2
- "name": "@dimcode/session-title",
3
- "version": "0.1.0",
4
- "lockfileVersion": 3,
5
- "requires": true,
6
- "packages": {
7
- "": {
8
- "name": "@dimcode/session-title",
9
- "version": "0.1.0",
10
- "dependencies": {
11
- "@arcships/rutis": "0.19.3"
12
- },
13
- "devDependencies": {
14
- "@types/node": "24.10.0"
15
- }
16
- },
17
- "node_modules/@arcships/rutis": {
18
- "version": "0.19.3",
19
- "resolved": "https://registry.npmjs.org/@arcships/rutis/-/rutis-0.19.3.tgz",
20
- "integrity": "sha512-mpeVQ2tIZFyiKibgmRtyEhFu0NW3xam/OMUWSkddXIj8a/QqyW3AiE37GKlb4ihpHtuRx1dIz9HyCYHgvnecJw==",
21
- "license": "MIT",
22
- "engines": {
23
- "bun": ">=1.4",
24
- "node": ">=22"
25
- }
26
- },
27
- "node_modules/@types/node": {
28
- "version": "24.10.0",
29
- "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.0.tgz",
30
- "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==",
31
- "dev": true,
32
- "license": "MIT",
33
- "dependencies": {
34
- "undici-types": "~7.16.0"
35
- }
36
- },
37
- "node_modules/undici-types": {
38
- "version": "7.16.0",
39
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
40
- "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
41
- "dev": true,
42
- "license": "MIT"
43
- }
44
- }
45
- }
@@ -1,9 +0,0 @@
1
- {
2
- "name": "@dimcode/session-title",
3
- "private": true,
4
- "version": "0.1.0",
5
- "type": "module",
6
- "exports": "./index.ts",
7
- "dependencies": { "@arcships/rutis": "0.19.3" },
8
- "devDependencies": { "@types/node": "24.10.0" }
9
- }