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.
- package/bin/dimcode +2 -2
- package/default-plugins.json +0 -1
- package/package.json +1 -1
- package/plugins/scheduler/README.md +4 -9
- package/plugins/scheduler/data.ts +8 -29
- package/plugins/scheduler/settings.ts +0 -9
- package/plugins/workflow/README.md +22 -19
- package/plugins/workflow/store.ts +61 -18
- package/release-manifest.json +9 -26
- package/plugins/session-title/README.md +0 -76
- package/plugins/session-title/dim-plugin.json +0 -11
- package/plugins/session-title/index.ts +0 -13
- package/plugins/session-title/node_modules/.package-lock.json +0 -18
- package/plugins/session-title/node_modules/@arcships/rutis/README.md +0 -32
- package/plugins/session-title/node_modules/@arcships/rutis/package.json +0 -46
- package/plugins/session-title/node_modules/@arcships/rutis/src/advanced.d.ts +0 -90
- package/plugins/session-title/node_modules/@arcships/rutis/src/advanced.mjs +0 -32
- package/plugins/session-title/node_modules/@arcships/rutis/src/index.d.ts +0 -43
- package/plugins/session-title/node_modules/@arcships/rutis/src/index.mjs +0 -71
- package/plugins/session-title/node_modules/@arcships/rutis/src/testing.d.ts +0 -77
- package/plugins/session-title/node_modules/@arcships/rutis/src/testing.mjs +0 -350
- package/plugins/session-title/node_modules/@arcships/rutis/src/values.mjs +0 -261
- package/plugins/session-title/package-lock.json +0 -45
- package/plugins/session-title/package.json +0 -9
- package/plugins/session-title/part.ts +0 -172
- 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
|
-
}
|