@genrojs/bag 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/browser.js ADDED
@@ -0,0 +1,7 @@
1
+ import * as TYTX from '@genrojs/tytx';
2
+
3
+ TYTX.setDecimalLibrary('number');
4
+
5
+ export {TYTX};
6
+ export * from './index.js';
7
+ export const version = '0.7.0';
@@ -0,0 +1,69 @@
1
+ /* Theme and layout tokens. */
2
+ :root{color-scheme:light;--bg:#fff;--text:#25313a;--muted:#77818c;--line:#e5e8eb;--focus:#2878b8}
3
+ @media(prefers-color-scheme:dark){:root:not([data-theme="light"]){color-scheme:dark;--bg:#171a1d;--text:#d5dbe0;--muted:#8f9aa4;--line:#30363c;--focus:#72b9ef}}
4
+ :root[data-theme="dark"]{color-scheme:dark;--bg:#171a1d;--text:#d5dbe0;--muted:#8f9aa4;--line:#30363c;--focus:#72b9ef}
5
+ .bag-view{--attribute-lane:190px;--lane-gap:10px;--indent:8px;background:var(--bg);color:var(--text);font:13px/1.4 system-ui,sans-serif;min-width:0}
6
+ .bag-nodes{list-style:none;margin:0;padding:0}
7
+ .bag-node{display:grid;grid-template-columns:minmax(0,1fr) var(--attribute-lane);column-gap:var(--lane-gap);align-items:start;min-width:0}
8
+ .bag-row{grid-column:1;display:grid;grid-template-columns:max-content minmax(90px,32%) minmax(0,1fr) auto;align-items:center;gap:2px;min-height:20px;padding:0 6px;min-width:0}
9
+ .bag-row:hover{background:#ffffff30}
10
+ .bag-label{font-weight:500;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-right:8px;color:#4d4940}
11
+ .bag-value{font:12px/1.45 ui-monospace,monospace;white-space:pre-wrap;overflow-wrap:anywhere;min-width:0;color:#38352e}
12
+ .type-badge{display:inline-flex;align-items:center;justify-content:center;justify-self:start;box-sizing:border-box;min-width:12px;height:12px;padding:0 2px;background:#888;color:#fff;border-radius:2px;font:500 8px/12px system-ui,sans-serif}
13
+ .node-tag{font-size:11px;font-style:italic;font-weight:400;margin-left:5px;white-space:nowrap}
14
+ .bag-meta{display:flex;justify-content:flex-end;align-items:center;min-width:0;font-size:11px}
15
+
16
+ /* Bag surfaces stop before the fixed attribute lane. */
17
+ .bag-branch{position:relative;isolation:isolate;border-radius:5px;margin:3px 0;--bag-surface:#f1f1f1;--right-inset:0px}
18
+ .bag-depth-1{--bag-surface:#e7e7e7}
19
+ .bag-branch::before{content:"";position:absolute;inset:0 calc(var(--attribute-lane) + var(--lane-gap) + var(--right-inset)) 0 0;z-index:-1;background:var(--bag-surface);border:1px solid #d5d5d5;border-radius:5px;pointer-events:none}
20
+ .bag-nodes .bag-nodes>.bag-branch{--right-inset:var(--indent)}
21
+ .bag-toggle{grid-column:1;margin-right:var(--right-inset);background:#707070;border-radius:4px;min-width:0}
22
+ .bag-toggle>summary{display:flex;align-items:center;position:relative;min-height:20px;padding:0 6px 0 22px;color:#fff;border-radius:4px}
23
+ .bag-toggle>summary:hover{background:#646464}
24
+ .bag-toggle>summary .bag-label{padding:0;color:inherit;flex:0 1 auto}
25
+ .bag-branch-name{display:flex;align-items:center;gap:5px;min-width:0}
26
+ .bag-count{display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;flex:none;min-width:14px;height:13px;padding:0 3px;border-radius:4px;background:#fff;color:#555;font:500 9px/12px system-ui,sans-serif}
27
+ .bag-branch>.bag-nodes{grid-column:1/-1;padding:3px 0 5px var(--indent)}
28
+ .bag-empty,.bag-cycle{grid-column:1/-1;color:#796f5c;font-size:12px;font-style:italic;margin:5px 0 7px 22px}
29
+ .bag-branch:has(>.bag-toggle:not([open]))>:is(.bag-nodes,.bag-empty,.bag-cycle){display:none}
30
+
31
+ /* Native disclosures share one centered, outlined arrow. */
32
+ .bag-view summary{cursor:pointer;list-style:none}
33
+ .bag-view summary::-webkit-details-marker{display:none}
34
+ .bag-view summary:focus-visible{outline:2px solid var(--focus);outline-offset:-2px;border-radius:2px}
35
+ :is(.bag-toggle,.bag-attributes)>summary::before{content:"";position:absolute;left:5px;top:50%;width:12px;height:12px;background:center/12px 12px no-repeat url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M4 2 L8 6 L4 10' fill='none' stroke='%23fff' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");transform:translateY(-50%);transition:transform .12s ease}
36
+ :is(.bag-toggle,.bag-attributes)[open]>summary::before{transform:translateY(-50%) rotate(90deg)}
37
+
38
+ /* Attributes use a separate, consistently aligned olive panel. */
39
+ .bag-attributes{grid-column:2;grid-row:1;min-width:0;margin:1px 0 2px;border-radius:5px;overflow:hidden}
40
+ .bag-attributes>summary{position:relative;box-sizing:border-box;height:17px;line-height:17px;padding:0 7px 0 20px;background:#7d8559;color:#fff;border-radius:5px;font-size:10px}
41
+ .bag-attributes>summary:hover{background:#6e764d}
42
+ .bag-attributes[open]>summary{border-radius:5px 5px 0 0}
43
+ .bag-attribute-count{margin-left:3px}
44
+ .bag-attributes>dl{display:grid;gap:1px;box-sizing:border-box;margin:0;padding:4px 8px 5px 21px;background:#f3f4e7;border:1px solid #dce0c5;border-top:0;border-radius:0 0 5px 5px}
45
+ .bag-attr{display:grid;grid-template-columns:minmax(0,1fr) minmax(40px,1fr);gap:8px;line-height:16px}
46
+ .bag-attr dt{display:grid;grid-template-columns:max-content minmax(0,1fr);align-items:center;gap:2px;color:#69704e;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
47
+ .bag-attr dd{margin:0;min-width:0}
48
+ .bag-attr code{font:11px/1.4 ui-monospace,monospace;color:#3f452e;overflow-wrap:anywhere}
49
+
50
+ /* Resolver details are inert: expanding never invokes a callback. */
51
+ .bag-resolver{position:relative;justify-self:end;align-self:start;z-index:3;font-size:10px;color:#675829}
52
+ .bag-branch>.bag-resolver{grid-column:1;grid-row:1;margin:2px calc(var(--right-inset) + 5px) 0 0}
53
+ .bag-branch:has(>.bag-resolver)>.bag-toggle{padding-right:110px}
54
+ .bag-resolver>summary{background:#fff1bb;color:#675829;border:1px solid #e8d997;border-radius:4px;padding:0 5px;font:10px/15px system-ui,sans-serif}
55
+ .bag-resolver>dl{box-sizing:border-box;width:220px;max-width:100%;margin:3px 0 0;padding:7px;background:#fff9df;color:#51482d;border:1px solid #e8d997;border-radius:4px}
56
+ .bag-resolver>dl>div{display:grid;grid-template-columns:90px 1fr;gap:8px;line-height:19px}
57
+ .bag-resolver dt,.bag-resolver dd{margin:0;overflow-wrap:anywhere}
58
+ .bag-no-types .bag-row{grid-template-columns:minmax(90px,32%) minmax(0,1fr) auto}
59
+ .bag-no-types .bag-attr dt{display:block}
60
+ @media(max-width:560px){
61
+ .bag-view{--attribute-lane:145px;--lane-gap:6px;--indent:5px}
62
+ .bag-row{grid-template-columns:max-content minmax(50px,1fr);padding-right:3px}
63
+ .bag-value{grid-column:2;font-size:11px}
64
+ .bag-label{white-space:normal}
65
+ .bag-meta{grid-column:2}
66
+ .bag-attr{grid-template-columns:1fr}
67
+ .bag-no-types .bag-row{grid-template-columns:minmax(50px,1fr) minmax(0,1fr)}
68
+ .bag-no-types .bag-value{grid-column:auto}
69
+ }
@@ -0,0 +1,78 @@
1
+ // Copyright 2026 Softwell S.r.l. - SPDX-License-Identifier: Apache-2.0
2
+ import { toTytx } from '@genrojs/tytx';
3
+
4
+ const TYPE_NAMES = {T:'Text',L:'Integer',R:'Float',N:'Decimal',B:'Boolean',D:'Date',H:'Time',HZ:'Time with timezone',DH:'Datetime',DHZ:'Datetime with timezone',NN:'Null',RAW:'Bytes',JS:'JSON',X:'Bag'};
5
+
6
+ /** Return an inert HTML fragment for a Bag without resolving or mutating it. */
7
+ export function htmlRepr(bag, options = {}) {
8
+ const {openDepth = 0, className = '', openAttributes = false, showTypes = true, title = 'Bag'} = options;
9
+ const ancestors = new Set();
10
+ const escape = value => String(value).replaceAll('&','&amp;').replaceAll('<','&lt;').replaceAll('>','&gt;').replaceAll('"','&quot;').replaceAll("'",'&#39;');
11
+ const isBag = value => value !== null && typeof value === 'object' && typeof value.getNodes === 'function';
12
+
13
+ function tytxInfo(value) {
14
+ let dtype, text;
15
+ try {
16
+ const encoded = toTytx(value);
17
+ if (typeof value === 'string') { dtype = 'T'; text = value; }
18
+ else if (value == null) { dtype = 'NN'; text = String(value); }
19
+ else if (typeof value === 'boolean') { dtype = 'B'; text = encoded; }
20
+ else if (typeof value === 'number') { dtype = Number.isInteger(value) ? 'L' : 'R'; text = String(value); }
21
+ else {
22
+ const suffix = typeof encoded === 'string' && encoded.match(/::([A-Z][A-Z0-9]*)$/);
23
+ dtype = suffix ? suffix[1] : 'JS';
24
+ text = suffix ? encoded.slice(0, -suffix[0].length) : encoded;
25
+ }
26
+ } catch { dtype = 'JS'; text = '[unserializable value]'; }
27
+ return {dtype, text:String(text), name:TYPE_NAMES[dtype] || 'TYTX value'};
28
+ }
29
+ function badge(dtype, name) {
30
+ if (!showTypes) return '';
31
+ const title = `${dtype} — ${name}`;
32
+ return `<span class="type-badge" data-dtype="${escape(dtype)}" title="${escape(title)}" aria-label="${escape(title)}">${escape(dtype)}</span>`;
33
+ }
34
+ function attributes(attrs) {
35
+ const entries = Object.entries(attrs || {});
36
+ if (!entries.length) return '';
37
+ const rows = entries.map(([key,value]) => {
38
+ const info = tytxInfo(value);
39
+ return `<div class="bag-attr"><dt>${badge(info.dtype,info.name)}<span>${escape(key)}</span></dt><dd><code>${escape(info.text)}</code></dd></div>`;
40
+ }).join('');
41
+ return `<details class="bag-attributes"${openAttributes ? ' open' : ''}><summary aria-label="${entries.length} attributes"><span class="bag-attribute-count">${entries.length}</span></summary><dl>${rows}</dl></details>`;
42
+ }
43
+ function renderBag(current, depth, nodes) {
44
+ if (ancestors.has(current)) return '<p class="bag-cycle" role="note">↻ circular reference</p>';
45
+ ancestors.add(current);
46
+ const html = nodes.length ? `<ol class="bag-nodes">${nodes.map(node => renderNode(node,depth)).join('')}</ol>` : '<p class="bag-empty">Empty Bag</p>';
47
+ ancestors.delete(current);
48
+ return html;
49
+ }
50
+ function resolverPanel(resolver) {
51
+ if (!resolver) return '';
52
+ const parameters = {cacheTime:resolver.cacheTime, readOnly:resolver.readOnly,
53
+ ...(options.resolverParameters ? options.resolverParameters(resolver) : {})};
54
+ const rows = Object.entries(parameters).map(([key,value]) => {
55
+ if (typeof value === 'function') {
56
+ return `<div><dt>${escape(key)}</dt><dd><code>${escape(value.name || '(anonymous)')}()</code></dd></div>`;
57
+ }
58
+ const info = tytxInfo(value);
59
+ return `<div><dt>${escape(key)}</dt><dd>${badge(info.dtype,info.name)} ${escape(info.text)}</dd></div>`;
60
+ }).join('');
61
+ return `<details class="bag-resolver" title="Static view: resolver was not evaluated"><summary>${escape(resolver.constructor.name)}</summary><dl>${rows}</dl></details>`;
62
+ }
63
+ function renderNode(node, depth) {
64
+ const value = node.staticValue;
65
+ const resolverBadge = resolverPanel(node.resolver);
66
+ const attrs = attributes(node.attr);
67
+ const tag = node.nodeTag ? `<span class="node-tag">${escape(node.nodeTag)}</span>` : '';
68
+ if (isBag(value)) {
69
+ const nodes = value.getNodes();
70
+ const count = nodes.length;
71
+ return `<li class="bag-node bag-branch bag-depth-${depth % 2}"><details class="bag-toggle"${depth < openDepth ? ' open' : ''}><summary><span class="bag-branch-name"><span class="bag-label">${escape(node.label)}</span><span class="bag-count" aria-label="${count} nodes">${count}</span>${tag}</span></summary></details>${resolverBadge}${attrs}${renderBag(value,depth + 1,nodes)}</li>`;
72
+ }
73
+ const info = tytxInfo(value);
74
+ return `<li class="bag-node bag-leaf"><div class="bag-row">${badge(info.dtype,info.name)}<span class="bag-label">${escape(node.label)}${tag}</span><code class="bag-value">${escape(info.text)}</code><span class="bag-meta">${resolverBadge}</span></div>${attrs}</li>`;
75
+ }
76
+ if (!isBag(bag)) throw new TypeError('htmlRepr expects a Bag-like object with getNodes()');
77
+ return `<div class="${escape(['bag-view',showTypes ? '' : 'bag-no-types',className].filter(Boolean).join(' '))}"><ol class="bag-nodes">${renderNode({label:title, staticValue:bag, attr:{}, resolver:null, nodeTag:null},0)}</ol></div>`;
78
+ }
@@ -0,0 +1 @@
1
+ export { htmlRepr } from './html-repr.js';
package/src/index.js ADDED
@@ -0,0 +1,9 @@
1
+ // Copyright 2025 Softwell S.r.l. - SPDX-License-Identifier: Apache-2.0
2
+
3
+ export { Bag, BagException } from './bag.js';
4
+ export { BagNode } from './bag-node.js';
5
+ export { BagNodeContainer } from './bag-node-container.js';
6
+ export { BagResolver, BagCbResolver, RETRY_POLICIES } from './resolver.js';
7
+ export { UrlResolver, UuidResolver, StorageResolver } from './resolvers/index.js';
8
+
9
+ export { BagSerializationError, OpaqueResolver, registerResolver } from './resolver-wire.js';
@@ -0,0 +1,128 @@
1
+ // Copyright 2026 Softwell S.r.l. - SPDX-License-Identifier: Apache-2.0
2
+
3
+ import { BagResolver } from './resolver.js';
4
+
5
+ /**
6
+ * Python-compatible resolver descriptions for TYTX, JSON and XML.
7
+ * Registration is explicit: adapters translate parameters between runtimes.
8
+ * Decoders must construct inert BagResolvers, never perform resolution or I/O.
9
+ * Unknown descriptions and signed tokens remain opaque and travel unchanged.
10
+ * A browser does not authenticate signed tokens; verification belongs to the
11
+ * server. Signed tokens are never used to instantiate a client-side resolver.
12
+ */
13
+ const MARKER = '::RSLV:';
14
+ const registrations = new Map();
15
+ const classes = new Map();
16
+ const opaquePayloads = new WeakMap();
17
+
18
+ export class BagSerializationError extends Error {
19
+ constructor(message) {
20
+ super(message);
21
+ this.name = 'BagSerializationError';
22
+ }
23
+ }
24
+
25
+ /** A server resolver description that can travel, but cannot execute locally. */
26
+ export class OpaqueResolver extends BagResolver {
27
+ constructor(payload) {
28
+ super({ asBag: false });
29
+ opaquePayloads.set(this, payload);
30
+ }
31
+
32
+ get payload() {
33
+ return opaquePayloads.get(this);
34
+ }
35
+
36
+ load() {
37
+ throw new BagSerializationError('This resolver is opaque; local execution is unavailable');
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Register a JS counterpart for a Python module/class pair.
43
+ * encode(resolver) returns {args: [], kwargs: {}}; decode({args, kwargs})
44
+ * returns an inert instance of cls. Names are wire identifiers, not imports.
45
+ */
46
+ export function registerResolver(cls, { module, name, encode, decode }) {
47
+ if (!(cls.prototype instanceof BagResolver) || cls === OpaqueResolver
48
+ || typeof module !== 'string' || !module || typeof name !== 'string' || !name
49
+ || typeof encode !== 'function' || typeof decode !== 'function') {
50
+ throw new TypeError('registerResolver requires a BagResolver subclass, module, name and adapters');
51
+ }
52
+ const key = JSON.stringify([module, name]);
53
+ if (registrations.has(key) && registrations.get(key).cls !== cls) {
54
+ throw new TypeError(`Resolver ${module}.${name} is already registered`);
55
+ }
56
+ if (classes.has(cls) && classes.get(cls).key !== key) {
57
+ throw new TypeError('A resolver class cannot own two wire identities');
58
+ }
59
+ const entry = { cls, module, name, encode, decode, key };
60
+ registrations.set(key, entry);
61
+ classes.set(cls, entry);
62
+ }
63
+
64
+ function validateDescription(data) {
65
+ if (!data || typeof data !== 'object' || Array.isArray(data)
66
+ || typeof data.resolver_module !== 'string' || typeof data.resolver_class !== 'string'
67
+ || !Array.isArray(data.args ?? []) || !data.kwargs
68
+ || typeof data.kwargs !== 'object' || Array.isArray(data.kwargs)) {
69
+ throw new BagSerializationError('Invalid resolver description');
70
+ }
71
+ }
72
+
73
+ function validateJson(value, parents = new Set()) {
74
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return;
75
+ if (typeof value === 'number' && Number.isFinite(value)) return;
76
+ if (typeof value !== 'object' || parents.has(value)
77
+ || (!Array.isArray(value) && Object.getPrototypeOf(value) !== Object.prototype
78
+ && Object.getPrototypeOf(value) !== null)) {
79
+ throw new BagSerializationError('Resolver parameters must be JSON data; callbacks cannot travel');
80
+ }
81
+ parents.add(value);
82
+ for (const child of Object.values(value)) validateJson(child, parents);
83
+ parents.delete(value);
84
+ }
85
+
86
+ export function encodeResolver(resolver) {
87
+ if (opaquePayloads.has(resolver)) return opaquePayloads.get(resolver);
88
+ const entry = classes.get(resolver.constructor);
89
+ if (!entry) throw new BagSerializationError(`Unregistered resolver: ${resolver.constructor.name}`);
90
+ const parameters = entry.encode(resolver);
91
+ const data = {
92
+ resolver_module: entry.module, resolver_class: entry.name,
93
+ args: parameters.args ?? [], kwargs: parameters.kwargs ?? {},
94
+ };
95
+ validateDescription(data);
96
+ validateJson(data);
97
+ return MARKER + JSON.stringify(data);
98
+ }
99
+
100
+ export function decodeResolver(value) {
101
+ if (typeof value !== 'string' || !value.startsWith(MARKER)) return null;
102
+ const payload = value.slice(MARKER.length);
103
+ // Preserve the exact signed token. Never trust its contents in the browser.
104
+ if (/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]*\.[A-Za-z0-9_-]+$/.test(payload)) {
105
+ return new OpaqueResolver(value);
106
+ }
107
+ let data;
108
+ try { data = JSON.parse(payload); }
109
+ catch { throw new BagSerializationError('Invalid resolver JSON or signed token'); }
110
+ validateDescription(data);
111
+ const entry = registrations.get(JSON.stringify([data.resolver_module, data.resolver_class]));
112
+ if (!entry) return new OpaqueResolver(value);
113
+ const resolver = entry.decode({ args: data.args ?? [], kwargs: data.kwargs });
114
+ if (!(resolver instanceof entry.cls)) {
115
+ throw new BagSerializationError('Resolver decoder returned an incompatible instance');
116
+ }
117
+ return resolver;
118
+ }
119
+
120
+ export function encodeAttrs(attr) {
121
+ return Object.fromEntries(Object.entries(attr || {}).map(([key, value]) =>
122
+ [key, value instanceof BagResolver ? encodeResolver(value) : value]));
123
+ }
124
+
125
+ export function decodeAttrs(attr) {
126
+ return Object.fromEntries(Object.entries(attr || {}).map(([key, value]) =>
127
+ [key, decodeResolver(value) ?? value]));
128
+ }