@genrojs/bag 0.10.3 → 0.11.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/README.md +4 -0
- package/docs/migration-ingestion.md +218 -0
- package/docs/python-alignment.md +12 -0
- package/package.json +4 -3
- package/src/bag.js +151 -155
- package/src/index.d.ts +11 -4
- package/src/resolver.js +14 -4
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# genro-bag-js
|
|
2
2
|
|
|
3
|
+
Current release: **0.11.1**. Fixes deletion origin propagation in `popNode`, `pop` and `delItem`.
|
|
4
|
+
|
|
5
|
+
> **0.11.0 — breaking API:** read the [ingestion and scheduling migration note](docs/migration-ingestion.md) before upgrading dependent projects.
|
|
6
|
+
|
|
3
7
|
TypeScript/JavaScript port of [genro-bag](https://github.com/nicofirst1/genro-bag) - hierarchical data container with XML serialization, Builder pattern, and Compiler system.
|
|
4
8
|
|
|
5
9
|
## Status
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Python 0.28.0 / JavaScript 0.11.0: explicit ingestion and no internal scheduling (breaking API)
|
|
2
|
+
|
|
3
|
+
This release changes Python and JavaScript together. Downstream projects must
|
|
4
|
+
review constructors, population calls, JSON readers, persisted Bag payloads and
|
|
5
|
+
scheduled resolver/subscription usage before upgrading. Related: [genro-bag #69](https://github.com/genro-org/genro-bag/issues/69).
|
|
6
|
+
|
|
7
|
+
## Constructor and replacement
|
|
8
|
+
|
|
9
|
+
```python
|
|
10
|
+
bag = Bag(source, recursive=True)
|
|
11
|
+
bag.fill_from(source, recursive=True) # replaces content; returns bag
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
```javascript
|
|
15
|
+
const bag = new Bag(source, {recursive: true});
|
|
16
|
+
bag.fillFrom(source, {recursive: true}); // replaces content; returns bag
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The root must be a dictionary/plain JS object, list/array, or `None`/`null`.
|
|
20
|
+
Omitting the source creates an empty Bag; omitting it in `fill_from`/`fillFrom`
|
|
21
|
+
clears the existing Bag. JS also treats `undefined` as an omitted source.
|
|
22
|
+
A Bag, tuple, scalar, serialized string, bytes, path, open file or URL is not a
|
|
23
|
+
valid constructor/population source. Unsupported inputs, including `0`, `False`
|
|
24
|
+
and `""`, raise an explicit error. Use `bag.deepcopy()` to copy a Bag.
|
|
25
|
+
|
|
26
|
+
With the default `recursive=True`, every nested dictionary/list is converted to
|
|
27
|
+
a fresh Bag. With `recursive=False`, only the root is converted: nested
|
|
28
|
+
containers remain the original shared objects. Other leaves are always shared,
|
|
29
|
+
including dates, Decimal values, binary buffers, class instances and existing
|
|
30
|
+
Bag values. Existing Bags keep their contents; their internal lists are not
|
|
31
|
+
reinterpreted. This is conversion, not a general deep copy. The existing
|
|
32
|
+
`deepcopy()` method copies Bag structure and resolvers; it does not promise to
|
|
33
|
+
clone every arbitrary leaf object.
|
|
34
|
+
|
|
35
|
+
Python uses `dict` and `list` (including subclasses); tuples are leaves. JS uses
|
|
36
|
+
arrays and objects whose prototype is `Object.prototype` or `null`. Other class
|
|
37
|
+
instances are leaves. No custom-type registry is consulted during conversion.
|
|
38
|
+
Normal resolver objects retain Bag's existing resolver attachment semantics.
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
source = {"items": [{"name": "Ada"}]}
|
|
42
|
+
Bag(source)["items.r_00.name"] # "Ada"
|
|
43
|
+
Bag(source, recursive=False)["items"] is source["items"] # True
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Repeated references on different branches produce separate converted Bags.
|
|
47
|
+
Cycles in dictionaries/lists raise an error identifying the conversion path
|
|
48
|
+
when recursion is enabled. Shallow ingestion does not traverse nested cycles.
|
|
49
|
+
Preparation failures in `fill_from`/`fillFrom` leave the old content intact;
|
|
50
|
+
successful attached replacement retains the existing single update event.
|
|
51
|
+
Listener failures after the replacement commits are not rolled back.
|
|
52
|
+
|
|
53
|
+
## Literal labels and list order
|
|
54
|
+
|
|
55
|
+
Dictionary keys must be strings and are inserted literally. `"a.b"`, `"?attr"`,
|
|
56
|
+
`"#0"` and the empty string are labels, not path expressions. Existing path-based
|
|
57
|
+
accessors still have their usual syntax: use node iteration or positional access
|
|
58
|
+
to inspect labels that contain path/query syntax. Assign paths explicitly with
|
|
59
|
+
`set_item`/`setItem` when you intend to create branches.
|
|
60
|
+
|
|
61
|
+
List labels start at zero and use `r_` plus a zero-padded index. Width is
|
|
62
|
+
`len(str(number_of_elements)) + 1`, calculated separately for each list:
|
|
63
|
+
|
|
64
|
+
| List length | Labels |
|
|
65
|
+
| --- | --- |
|
|
66
|
+
| 5 | `r_00` … `r_04` |
|
|
67
|
+
| 25 | `r_000` … `r_024` |
|
|
68
|
+
| 150 | `r_0000` … `r_0149` |
|
|
69
|
+
|
|
70
|
+
This preserves initial order under lexicographic sorting. Labels are not
|
|
71
|
+
renumbered when a Bag is edited later. Converting a different-sized list may
|
|
72
|
+
choose another width. Empty dictionaries and lists both become empty Bags.
|
|
73
|
+
Lists of tuples/arrays are data, never implicit node descriptions.
|
|
74
|
+
|
|
75
|
+
## One reader for JSON and TYTX
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
Bag.from_tytx(text, recursive=True, sign_key=None)
|
|
79
|
+
Bag.from_tytx(binary, transport="msgpack", recursive=False)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
```javascript
|
|
83
|
+
Bag.fromTytx(text, {recursive: true});
|
|
84
|
+
Bag.fromTytx(binary, 'msgpack', {recursive: false});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Ordinary JSON and generic TYTX dictionaries/lists use the constructor's
|
|
88
|
+
conversion rules. Typed values encoded by TYTX are restored before conversion.
|
|
89
|
+
Ordinary JSON cannot restore dates or decimals that were reduced to ordinary
|
|
90
|
+
strings by its producer. Scalar roots are rejected, including JSON `null`.
|
|
91
|
+
|
|
92
|
+
A serialized Bag is explicitly identified by its type marker (`::X` for Bag).
|
|
93
|
+
It is reconstructed with its labels, attributes, tags and resolvers; `recursive`
|
|
94
|
+
does not reinterpret its stored list/dictionary values. The Python reader
|
|
95
|
+
continues to verify resolver signatures when `sign_key` is supplied, including
|
|
96
|
+
registered Bag payloads decoded through TYTX. Signed resolver tokens remain
|
|
97
|
+
opaque in JS and can return unchanged to Python.
|
|
98
|
+
|
|
99
|
+
`from_json` / `fromJson` is deprecated and now delegates to the new reader.
|
|
100
|
+
Python emits `DeprecationWarning`; JS warns once per module instance. The alias
|
|
101
|
+
accepts serialized input, not native objects. `list_joiner` / `listJoiner` is
|
|
102
|
+
removed from this API. Normal fields such as `label`, `value`, `attr`, `resolver`
|
|
103
|
+
and `rows` have no special meaning in generic input.
|
|
104
|
+
|
|
105
|
+
## Serialized Bag format
|
|
106
|
+
|
|
107
|
+
`bag.to_tytx()` / `bag.toTytx()` now emits the outer Bag marker exactly once.
|
|
108
|
+
Generic `genro_tytx.to_tytx(bag)` / `toTytx(bag)` uses separate registry payload
|
|
109
|
+
hooks and produces the same type identity. MessagePack uses TYTX custom-type
|
|
110
|
+
extension 4, carrying the Bag suffix and its JSON TYTX payload.
|
|
111
|
+
|
|
112
|
+
Within Bag rows, parent paths are arrays of literal label segments; compact
|
|
113
|
+
path dictionaries also contain segment arrays. This distinguishes a label
|
|
114
|
+
`"a.b"` from two labels `"a"`, `"b"`. The marked reader still accepts the old
|
|
115
|
+
string parent paths inside an explicitly marked payload.
|
|
116
|
+
|
|
117
|
+
Old **unmarked** `{rows: ...}` payloads are now generic application dictionaries.
|
|
118
|
+
For a known old JSON Bag file, migrate offline by adding its `::X` marker; never
|
|
119
|
+
infer that an arbitrary input is a Bag from a `rows` key. For old MessagePack Bag
|
|
120
|
+
files, read with the previous release and write the new format with an explicit
|
|
121
|
+
migration tool. Keep payload provenance and resolver-signature requirements.
|
|
122
|
+
|
|
123
|
+
Existing custom Bag subtype registrations must register the payload hooks with
|
|
124
|
+
TYTX's `register_type` / `registerType`, not register the public marked methods:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
register_type(MyBag, MyBag.__tytx_suffix__,
|
|
128
|
+
lambda bag: bag._to_tytx_payload(), MyBag._from_tytx_payload)
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
```javascript
|
|
132
|
+
registerType(MyBag, MyBag.tytxSuffix,
|
|
133
|
+
bag => bag._toTytxPayload(), payload => MyBag._fromTytxPayload(payload));
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`to_json` / `toJson` still exports the historical node-description format.
|
|
137
|
+
It is **not** the inverse of the deprecated reader anymore. Use the TYTX pair
|
|
138
|
+
for a Bag round trip. The Python Genropy compatibility adapter retains the
|
|
139
|
+
historical node-description decoder and its signature checks.
|
|
140
|
+
|
|
141
|
+
## Call-site migration
|
|
142
|
+
|
|
143
|
+
| Old use | New use |
|
|
144
|
+
| --- | --- |
|
|
145
|
+
| `Bag(other_bag)` | `other_bag.deepcopy()` |
|
|
146
|
+
| `Bag(text)` / `fill_from(text)` | `Bag.from_tytx(text)` or `Bag.from_xml(text)` |
|
|
147
|
+
| `Bag(path)` / `fill_from(path)` | `Bag.from_file(path)` in Python |
|
|
148
|
+
| Native dict/list passed to `from_json` / `fromJson` | Constructor |
|
|
149
|
+
| Array kept as a value by the old constructor | Explicit `recursive=False`, or assign using `set_item` / `setItem` |
|
|
150
|
+
| `to_json` → `from_json` Bag round trip | `to_tytx` → `from_tytx` |
|
|
151
|
+
| Implicit node descriptions in lists | Explicit `set_item` / `setItem` calls |
|
|
152
|
+
| Old numeric or field-prefixed list paths | New padded `r_…` labels |
|
|
153
|
+
|
|
154
|
+
No `from_data`/`fromData` or `from_nodes`/`fromNodes` is introduced. Compression
|
|
155
|
+
and whole-document authentication are a separate TYTX proposal (#59).
|
|
156
|
+
|
|
157
|
+
## No internal scheduling
|
|
158
|
+
|
|
159
|
+
Bag and its resolvers no longer schedule background work. This applies in both
|
|
160
|
+
Python and JavaScript:
|
|
161
|
+
|
|
162
|
+
- `subscribe` and `unsubscribe` reject `timer` and `interval` options. The `tmr`
|
|
163
|
+
event and its propagation have been removed.
|
|
164
|
+
- Resolver construction rejects `interval` and `reactive`, including
|
|
165
|
+
`interval=None` / `null` and `reactive=False` / `false`. Remove obsolete
|
|
166
|
+
defaults from custom resolver classes and previously serialized descriptors.
|
|
167
|
+
Reissue signed descriptors on the server; do not edit signed client tokens.
|
|
168
|
+
- `reset()` takes no arguments. `reset(refresh=True)` and JS
|
|
169
|
+
`reset({refresh: true})` raise `TypeError` instead of scheduling a reload.
|
|
170
|
+
- Attaching, detaching, expiring or invalidating a resolver never loads it.
|
|
171
|
+
Attribute changes are used on the next explicit read. A read updates the
|
|
172
|
+
cache silently; it does not emit a value-change event.
|
|
173
|
+
- Mutation subscriptions remain synchronous. Python keeps the synchronous resolver contract published in 0.23.0; JS also
|
|
174
|
+
accepts explicit asynchronous reads. Passive TTL caches, infinite caches and
|
|
175
|
+
explicit invalidation remain.
|
|
176
|
+
Retry delays belong to a requested load; they do
|
|
177
|
+
not start a background polling task.
|
|
178
|
+
|
|
179
|
+
The application owns polling, cancellation, error handling and publication.
|
|
180
|
+
For example, an application scheduler can call this function:
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
def poll_once(resolver, output_bag):
|
|
184
|
+
resolver.reset()
|
|
185
|
+
value = resolver()
|
|
186
|
+
output_bag.set_item('result', value) # Explicit mutation notifies subscribers
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
```javascript
|
|
190
|
+
async function pollOnce(resolver, outputBag) {
|
|
191
|
+
resolver.reset();
|
|
192
|
+
const value = await resolver.resolve();
|
|
193
|
+
outputBag.setItem('result', value);
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Use a separate output node for published results. Explicitly writing a resolved
|
|
198
|
+
value can replace an existing resolver attached to that destination node.
|
|
199
|
+
|
|
200
|
+
In JS, node attributes now override resolver defaults on the next read, matching
|
|
201
|
+
Python. Explicit call parameters still take precedence. Cache sentinels remain aligned with the published versions: both Python and JS
|
|
202
|
+
use a negative TTL for infinite caching and reject boolean cache durations.
|
|
203
|
+
Python's synchronous parameter preparation, readonly cache semantics, fired
|
|
204
|
+
events and TYTX `__cls` subtype metadata from releases through 0.27.0 are preserved.
|
|
205
|
+
|
|
206
|
+
Python retains `replace(Bag)` and explicit legacy codecs (`legacy_mode=True`).
|
|
207
|
+
Its deprecated camelCase `fillFrom` remains a compatibility source loader;
|
|
208
|
+
the modern `fill_from` accepts native dictionaries/lists only. Old constructor
|
|
209
|
+
keyword items must become a dictionary: `Bag(name="Ada")` becomes
|
|
210
|
+
`Bag({"name": "Ada"})`. Node tuples must be inserted explicitly with `set_item`;
|
|
211
|
+
use the GenroPy adapter when the historical polymorphic constructor is required.
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
## Validation and dependency note
|
|
215
|
+
|
|
216
|
+
Both implementations use TYTX 0.16.2. Python requires Toolbox >=0.18.0; obsolete
|
|
217
|
+
timer imports have been removed. Validation runs against Toolbox 0.18.0 without
|
|
218
|
+
falling back to 0.14.
|
package/docs/python-alignment.md
CHANGED
|
@@ -154,3 +154,15 @@ Zero, false and empty strings are preserved in either mode. `Bag.setItem`
|
|
|
154
154
|
forwards its existing null-removal option when constructing a new node as well
|
|
155
155
|
as when updating one. This is an optional argument and a propagation bug fix,
|
|
156
156
|
not a new default policy. See `constructor-null-attributes.test.js`.
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
## Passive resolvers and application scheduling
|
|
160
|
+
|
|
161
|
+
Bag does not own a scheduler in either language. Resolver `interval` and
|
|
162
|
+
`reactive` options and subscription `timer` / `interval` options raise TypeError.
|
|
163
|
+
`reset()` only invalidates the cache, with no refresh argument. Attachment,
|
|
164
|
+
attribute changes and TTL expiry do not start background loads. Node attributes
|
|
165
|
+
override resolver defaults on the next read; explicit call parameters have the
|
|
166
|
+
highest priority. Applications schedule polling and publish results with
|
|
167
|
+
`setItem` when observers need a mutation event. See the
|
|
168
|
+
[migration note](migration-ingestion.md#no-internal-scheduling).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genrojs/bag",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "JavaScript port of genro-bag - hierarchical data container",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"README.md",
|
|
19
19
|
"LICENSE",
|
|
20
20
|
"NOTICE",
|
|
21
|
-
"docs/python-alignment.md"
|
|
21
|
+
"docs/python-alignment.md",
|
|
22
|
+
"docs/migration-ingestion.md"
|
|
22
23
|
],
|
|
23
24
|
"scripts": {
|
|
24
25
|
"test": "node --test tests/*.test.js",
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
"node": ">=22"
|
|
42
43
|
},
|
|
43
44
|
"dependencies": {
|
|
44
|
-
"@genrojs/tytx": "^0.16.
|
|
45
|
+
"@genrojs/tytx": "^0.16.2",
|
|
45
46
|
"@msgpack/msgpack": "^3.1.3",
|
|
46
47
|
"@xmldom/xmldom": "^0.9.12",
|
|
47
48
|
"big.js": "^7.0.1"
|
package/src/bag.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import { BagNodeContainer } from './bag-node-container.js';
|
|
4
4
|
import { BagNode } from './bag-node.js';
|
|
5
|
-
import { toTytx as tytxEncode, fromTytx as tytxDecode,
|
|
5
|
+
import { toTytx as tytxEncode, fromTytx as tytxDecode, registerType, getRegisteredType, getSubtypeDict, setSubtypeDict } from '@genrojs/tytx';
|
|
6
|
+
import { encode as msgpackEncode, ExtData } from '@msgpack/msgpack';
|
|
6
7
|
import { DOMParser as XmlDOMParser } from '@xmldom/xmldom';
|
|
7
8
|
import { BagCbResolver, BagResolver } from './resolver.js';
|
|
8
9
|
import { BagSerializationError, encodeResolver, decodeResolver, encodeAttrs, decodeAttrs } from './resolver-wire.js';
|
|
@@ -10,9 +11,9 @@ import { CLS_ATTRIBUTE, getClsMarker, getInheritedClass, getSubtypeClass } from
|
|
|
10
11
|
|
|
11
12
|
// Resolver caches are not structural branches on the wire: the descriptor
|
|
12
13
|
// owns that node, and serializing cached descendants would orphan them.
|
|
13
|
-
function* serializationNodes(bag, prefix =
|
|
14
|
+
function* serializationNodes(bag, prefix = []) {
|
|
14
15
|
for (const node of bag) {
|
|
15
|
-
const path = prefix
|
|
16
|
+
const path = [...prefix, node.label];
|
|
16
17
|
yield [path, node];
|
|
17
18
|
const value = node.getValue(true);
|
|
18
19
|
if (!node.resolver && value instanceof Bag) yield* serializationNodes(value, path);
|
|
@@ -35,6 +36,28 @@ function xmlDateText(value) {
|
|
|
35
36
|
return timestamp;
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
function isDictionary(value) {
|
|
40
|
+
if (value === null || typeof value !== 'object') return false;
|
|
41
|
+
const proto = Object.getPrototypeOf(value);
|
|
42
|
+
return proto === Object.prototype || proto === null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function wireValue(value) {
|
|
46
|
+
if (typeof value === 'string') return value.includes('::') ? `${value}::T` : value;
|
|
47
|
+
if (Array.isArray(value)) return value.map(wireValue);
|
|
48
|
+
if (isDictionary(value)) return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, wireValue(v)]));
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function ingestionOptions(options) {
|
|
53
|
+
if (!isDictionary(options)) throw new TypeError('Bag options must be an object');
|
|
54
|
+
const { recursive = true } = options;
|
|
55
|
+
if (typeof recursive !== 'boolean') throw new TypeError('recursive must be a boolean');
|
|
56
|
+
return recursive;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let warnedFromJson = false;
|
|
60
|
+
|
|
38
61
|
/**
|
|
39
62
|
* Bag - Hierarchical data container with path-based access.
|
|
40
63
|
*
|
|
@@ -48,9 +71,10 @@ export class Bag {
|
|
|
48
71
|
/**
|
|
49
72
|
* Create a new Bag.
|
|
50
73
|
*
|
|
51
|
-
* @param {Object} [source=null] -
|
|
74
|
+
* @param {Object|Array|null} [source=null] - In-memory data with literal string labels.
|
|
75
|
+
* @param {{recursive?: boolean}} [options={}] - Convert nested containers (default true).
|
|
52
76
|
*/
|
|
53
|
-
constructor(source = null) {
|
|
77
|
+
constructor(source = null, options = {}) {
|
|
54
78
|
this._nodes = new BagNodeContainer();
|
|
55
79
|
this._backref = false;
|
|
56
80
|
this._parent = null;
|
|
@@ -61,8 +85,9 @@ export class Bag {
|
|
|
61
85
|
this._insSubscribers = {};
|
|
62
86
|
this._delSubscribers = {};
|
|
63
87
|
|
|
64
|
-
|
|
65
|
-
|
|
88
|
+
ingestionOptions(options);
|
|
89
|
+
if (source !== null && source !== undefined) {
|
|
90
|
+
this.fillFrom(source, options);
|
|
66
91
|
}
|
|
67
92
|
}
|
|
68
93
|
|
|
@@ -432,7 +457,7 @@ export class Bag {
|
|
|
432
457
|
if (p >= 0) {
|
|
433
458
|
const node = this._nodes.pop(p);
|
|
434
459
|
if (this.backref) {
|
|
435
|
-
this._onNodeDeleted(node, p, reason);
|
|
460
|
+
this._onNodeDeleted(node, p, null, reason);
|
|
436
461
|
}
|
|
437
462
|
return node;
|
|
438
463
|
}
|
|
@@ -717,7 +742,11 @@ export class Bag {
|
|
|
717
742
|
* @param {Function} [options.delete] - Callback for delete events.
|
|
718
743
|
* @param {Function} [options.any] - Callback for all events.
|
|
719
744
|
*/
|
|
720
|
-
subscribe(subscriberId,
|
|
745
|
+
subscribe(subscriberId, options = {}) {
|
|
746
|
+
if (Object.hasOwn(options, 'timer') || Object.hasOwn(options, 'interval')) {
|
|
747
|
+
throw new TypeError('Timer subscriptions were removed. Schedule polling in the application.');
|
|
748
|
+
}
|
|
749
|
+
const { update = null, insert = null, delete: del = null, any = null } = options;
|
|
721
750
|
if (!this.backref) {
|
|
722
751
|
this.setBackref();
|
|
723
752
|
}
|
|
@@ -737,7 +766,11 @@ export class Bag {
|
|
|
737
766
|
* @param {boolean} [options.delete=false] - Remove delete subscription.
|
|
738
767
|
* @param {boolean} [options.any=false] - Remove all subscriptions.
|
|
739
768
|
*/
|
|
740
|
-
unsubscribe(subscriberId,
|
|
769
|
+
unsubscribe(subscriberId, options = {}) {
|
|
770
|
+
if (Object.hasOwn(options, 'timer') || Object.hasOwn(options, 'interval')) {
|
|
771
|
+
throw new TypeError('Timer subscriptions were removed. Schedule polling in the application.');
|
|
772
|
+
}
|
|
773
|
+
const { update = false, insert = false, delete: del = false, any = false } = options;
|
|
741
774
|
if (update || any) {
|
|
742
775
|
delete this._updSubscribers[subscriberId];
|
|
743
776
|
}
|
|
@@ -1633,57 +1666,56 @@ export class Bag {
|
|
|
1633
1666
|
* @param {Object} data - Object where keys become labels and values become node values.
|
|
1634
1667
|
* @private
|
|
1635
1668
|
*/
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
}
|
|
1645
|
-
}
|
|
1669
|
+
_appendLiteral(label, value, attr = null, resolver = null, nodeTag = null) {
|
|
1670
|
+
if (typeof label !== 'string') throw new TypeError('Bag labels must be strings');
|
|
1671
|
+
const node = new this.nodeClass(this, label, null, attr, resolver, nodeTag, null, false);
|
|
1672
|
+
if (value instanceof BagResolver) node.resolver = value;
|
|
1673
|
+
else node._value = value;
|
|
1674
|
+
this._nodes._dict[label] = node;
|
|
1675
|
+
this._nodes._list.push(node);
|
|
1676
|
+
return node;
|
|
1646
1677
|
}
|
|
1647
1678
|
|
|
1648
|
-
/**
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
* - If source is a Bag: copies all nodes (deep copy for nested Bags)
|
|
1654
|
-
* - If source is a plain object: keys become labels, values become node values.
|
|
1655
|
-
* Nested objects are recursively converted to Bags.
|
|
1656
|
-
*
|
|
1657
|
-
* Preparation errors leave this Bag unchanged. Attached Bags emit one
|
|
1658
|
-
* upd_value event after replacement, carrying the previous content.
|
|
1659
|
-
* Listener exceptions propagate after the replacement has committed.
|
|
1660
|
-
*
|
|
1661
|
-
* @param {Bag|Object|null} source - Source to fill from.
|
|
1662
|
-
* @returns {Bag} This Bag (for chaining).
|
|
1663
|
-
*
|
|
1664
|
-
* @example
|
|
1665
|
-
* const bag = new Bag();
|
|
1666
|
-
* bag.replace(new Bag({ a: 1, b: { c: 2 } }));
|
|
1667
|
-
* // bag has 'a' = 1, 'b' = Bag with 'c' = 2
|
|
1668
|
-
*
|
|
1669
|
-
* const other = new Bag();
|
|
1670
|
-
* other.setItem('x', 10);
|
|
1671
|
-
* bag.replace(other);
|
|
1672
|
-
* // bag now has only 'x' = 10
|
|
1673
|
-
*/
|
|
1674
|
-
_loadSource(source) {
|
|
1675
|
-
if (source == null) return this;
|
|
1676
|
-
const prepared = this.createChildBag();
|
|
1677
|
-
if (source instanceof Bag) {
|
|
1678
|
-
prepared._fillFromBag(source);
|
|
1679
|
-
} else if (typeof source === 'object' && !Array.isArray(source)) {
|
|
1680
|
-
prepared._fillFromDict(source);
|
|
1681
|
-
} else {
|
|
1682
|
-
return this;
|
|
1679
|
+
/** Replace in-memory data atomically. None/null clears; leaves stay shared. */
|
|
1680
|
+
fillFrom(source = null, options = {}) {
|
|
1681
|
+
const recursive = ingestionOptions(options);
|
|
1682
|
+
if (source != null && !isDictionary(source) && !Array.isArray(source)) {
|
|
1683
|
+
throw new TypeError('Bag source must be a dictionary, array or null');
|
|
1683
1684
|
}
|
|
1685
|
+
const prepared = this.createChildBag();
|
|
1686
|
+
const active = new Set();
|
|
1687
|
+
const populate = (bag, data, path) => {
|
|
1688
|
+
if (active.has(data)) throw new TypeError(`Cyclic Bag source at ${path}`);
|
|
1689
|
+
active.add(data);
|
|
1690
|
+
try {
|
|
1691
|
+
const width = Array.isArray(data) ? String(data.length).length + 1 : 0;
|
|
1692
|
+
const entries = Array.isArray(data)
|
|
1693
|
+
? Array.from(data, (value, i) => [`r_${String(i).padStart(width, '0')}`, value])
|
|
1694
|
+
: Object.entries(data);
|
|
1695
|
+
if (!Array.isArray(data) && Object.getOwnPropertySymbols(data)
|
|
1696
|
+
.some(key => Object.prototype.propertyIsEnumerable.call(data, key))) {
|
|
1697
|
+
throw new TypeError(`Bag labels must be strings at ${path}`);
|
|
1698
|
+
}
|
|
1699
|
+
for (const [label, item] of entries) {
|
|
1700
|
+
let value = item;
|
|
1701
|
+
if (recursive && (isDictionary(value) || Array.isArray(value))) {
|
|
1702
|
+
value = this.createChildBag();
|
|
1703
|
+
populate(value, item, `${path}[${JSON.stringify(label)}]`);
|
|
1704
|
+
}
|
|
1705
|
+
bag._appendLiteral(label, value);
|
|
1706
|
+
}
|
|
1707
|
+
} finally {
|
|
1708
|
+
active.delete(data);
|
|
1709
|
+
}
|
|
1710
|
+
};
|
|
1711
|
+
if (source != null) populate(prepared, source, '$');
|
|
1684
1712
|
return this._replacePrepared(prepared);
|
|
1685
1713
|
}
|
|
1686
1714
|
|
|
1715
|
+
_loadSource(source, options = {}) {
|
|
1716
|
+
return this.fillFrom(source, options);
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1687
1719
|
/** Replace all contents from a Bag, preserving identity. Returns this. */
|
|
1688
1720
|
replace(other) {
|
|
1689
1721
|
if (!(other instanceof Bag)) throw new TypeError('Bag.replace expects a Bag');
|
|
@@ -1787,11 +1819,12 @@ export class Bag {
|
|
|
1787
1819
|
let codeCounter = 0;
|
|
1788
1820
|
// Class of each branch by path: a node's parent class decides whether
|
|
1789
1821
|
// its branch needs __cls (parentBag is null when the backref is off).
|
|
1790
|
-
const pathToClass = new Map([['', this.constructor]]);
|
|
1822
|
+
const pathToClass = new Map([['[]', this.constructor]]);
|
|
1791
1823
|
|
|
1792
1824
|
for (const [path, node] of serializationNodes(this)) {
|
|
1793
|
-
const
|
|
1794
|
-
const
|
|
1825
|
+
const parentPath = path.slice(0, -1);
|
|
1826
|
+
const pathKey = JSON.stringify(path);
|
|
1827
|
+
const parentKey = JSON.stringify(parentPath);
|
|
1795
1828
|
|
|
1796
1829
|
const nodeValue = node.getValue(true);
|
|
1797
1830
|
|
|
@@ -1803,29 +1836,29 @@ export class Bag {
|
|
|
1803
1836
|
} else if (nodeValue instanceof Bag) {
|
|
1804
1837
|
const cls = nodeValue.constructor;
|
|
1805
1838
|
const suffix = cls.tytxSuffix;
|
|
1806
|
-
pathToClass.set(
|
|
1807
|
-
branchCls = getClsMarker(cls, getInheritedClass(pathToClass.get(
|
|
1839
|
+
pathToClass.set(pathKey, cls);
|
|
1840
|
+
branchCls = getClsMarker(cls, getInheritedClass(pathToClass.get(parentKey), suffix));
|
|
1808
1841
|
value = `::${suffix}`;
|
|
1809
1842
|
} else if (nodeValue === null) {
|
|
1810
1843
|
value = '::NN';
|
|
1811
1844
|
} else {
|
|
1812
|
-
value = nodeValue;
|
|
1845
|
+
value = wireValue(nodeValue);
|
|
1813
1846
|
}
|
|
1814
1847
|
|
|
1815
1848
|
if (node.attr && Object.hasOwn(node.attr, CLS_ATTRIBUTE)) {
|
|
1816
1849
|
throw new BagSerializationError(
|
|
1817
1850
|
`node '${path}': the attribute '${CLS_ATTRIBUTE}' is reserved for the Bag class name`);
|
|
1818
1851
|
}
|
|
1819
|
-
const attr = encodeAttrs(node.attr);
|
|
1852
|
+
const attr = encodeAttrs(wireValue(node.attr));
|
|
1820
1853
|
if (branchCls !== null) attr[CLS_ATTRIBUTE] = branchCls;
|
|
1821
1854
|
const tag = node.nodeTag;
|
|
1822
1855
|
|
|
1823
1856
|
if (compact) {
|
|
1824
|
-
const parentRef = parentPath ? pathToCode[
|
|
1857
|
+
const parentRef = parentPath.length ? pathToCode[parentKey] : null;
|
|
1825
1858
|
yield [parentRef, node.label, tag, value, attr];
|
|
1826
1859
|
|
|
1827
1860
|
if (nodeValue instanceof Bag) {
|
|
1828
|
-
pathToCode[
|
|
1861
|
+
pathToCode[pathKey] = codeCounter;
|
|
1829
1862
|
pathRegistry[codeCounter] = path;
|
|
1830
1863
|
codeCounter++;
|
|
1831
1864
|
}
|
|
@@ -1847,6 +1880,16 @@ export class Bag {
|
|
|
1847
1880
|
* @returns {string|Uint8Array} Serialized data.
|
|
1848
1881
|
*/
|
|
1849
1882
|
toTytx(transport = 'json', compact = false) {
|
|
1883
|
+
const payload = this._toTytxPayload(compact);
|
|
1884
|
+
const suffix = this.constructor.tytxSuffix;
|
|
1885
|
+
if (transport === 'json') return `${payload}::${suffix}`;
|
|
1886
|
+
if (transport === 'msgpack') {
|
|
1887
|
+
return msgpackEncode(new ExtData(4, new TextEncoder().encode(`${suffix}:${payload}`)));
|
|
1888
|
+
}
|
|
1889
|
+
throw new TypeError(`Unsupported transport: ${transport}`);
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
_toTytxPayload(compact = false) {
|
|
1850
1893
|
let data;
|
|
1851
1894
|
|
|
1852
1895
|
if (compact) {
|
|
@@ -1861,8 +1904,10 @@ export class Bag {
|
|
|
1861
1904
|
const rootCls = getClsMarker(rootClass, getInheritedClass(null, rootClass.tytxSuffix));
|
|
1862
1905
|
if (rootCls !== null) data[CLS_ATTRIBUTE] = rootCls;
|
|
1863
1906
|
|
|
1864
|
-
|
|
1865
|
-
|
|
1907
|
+
data.rows = data.rows.map(([parent, label, tag, value, attr]) =>
|
|
1908
|
+
[wireValue(parent), wireValue(label), wireValue(tag), value, attr]);
|
|
1909
|
+
if (data.paths) data.paths = wireValue(data.paths);
|
|
1910
|
+
return tytxEncode(data);
|
|
1866
1911
|
}
|
|
1867
1912
|
|
|
1868
1913
|
/**
|
|
@@ -1872,13 +1917,31 @@ export class Bag {
|
|
|
1872
1917
|
* @param {string} [transport='json'] - Input format: 'json' or 'msgpack'.
|
|
1873
1918
|
* @returns {Bag} Reconstructed Bag.
|
|
1874
1919
|
*/
|
|
1875
|
-
static fromTytx(data, transport = 'json') {
|
|
1920
|
+
static fromTytx(data, transport = 'json', options = {}) {
|
|
1921
|
+
if (isDictionary(transport)) { options = transport; transport = 'json'; }
|
|
1922
|
+
ingestionOptions(options);
|
|
1923
|
+
if (typeof data !== 'string' && !(data instanceof Uint8Array)) {
|
|
1924
|
+
throw new TypeError('fromTytx requires serialized text or bytes');
|
|
1925
|
+
}
|
|
1926
|
+
if (transport !== 'json' && transport !== 'msgpack') {
|
|
1927
|
+
throw new TypeError(`Unsupported transport: ${transport}`);
|
|
1928
|
+
}
|
|
1929
|
+
if (transport === 'json' && data instanceof Uint8Array) data = new TextDecoder().decode(data);
|
|
1930
|
+
const decoded = tytxDecode(data, transport === 'json' ? null : transport);
|
|
1931
|
+
if (decoded instanceof Bag) return decoded;
|
|
1932
|
+
if (!isDictionary(decoded) && !Array.isArray(decoded)) {
|
|
1933
|
+
throw new TypeError('fromTytx requires a dictionary, array or typed Bag root');
|
|
1934
|
+
}
|
|
1935
|
+
return new this(decoded, options);
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
static _fromTytxPayload(data) {
|
|
1876
1939
|
// The payload alone decides the class of the root: without __cls it is
|
|
1877
1940
|
// the class registered for the suffix (Bag for "X"), whatever this is.
|
|
1878
1941
|
const suffix = this.tytxSuffix;
|
|
1879
1942
|
// The registry passes an empty payload for a structural branch marker.
|
|
1880
1943
|
if (data === '') return new (getInheritedClass(null, suffix))();
|
|
1881
|
-
const parsed = tytxDecode(data
|
|
1944
|
+
const parsed = tytxDecode(data);
|
|
1882
1945
|
if (!parsed || !Array.isArray(parsed.rows)) {
|
|
1883
1946
|
throw new BagSerializationError('Invalid TYTX Bag: expected rows');
|
|
1884
1947
|
}
|
|
@@ -1888,7 +1951,7 @@ export class Bag {
|
|
|
1888
1951
|
? getSubtypeClass(suffix, parsed[CLS_ATTRIBUTE])
|
|
1889
1952
|
: getInheritedClass(null, suffix);
|
|
1890
1953
|
const bag = new rootClass();
|
|
1891
|
-
const pathToBag = new Map([['', bag]]);
|
|
1954
|
+
const pathToBag = new Map([['[]', bag]]);
|
|
1892
1955
|
|
|
1893
1956
|
for (const row of parsed.rows) {
|
|
1894
1957
|
if (!Array.isArray(row) || row.length !== 5) {
|
|
@@ -1902,18 +1965,15 @@ export class Bag {
|
|
|
1902
1965
|
}
|
|
1903
1966
|
parentPath = parentRef === null ? '' : paths[parentRef];
|
|
1904
1967
|
}
|
|
1905
|
-
if (
|
|
1968
|
+
if (typeof parentPath === 'string') parentPath = parentPath ? parentPath.split('.') : [];
|
|
1969
|
+
if (!Array.isArray(parentPath)) throw new BagSerializationError('Invalid TYTX parent path');
|
|
1970
|
+
const parentKey = JSON.stringify(parentPath);
|
|
1971
|
+
if (!pathToBag.has(parentKey)) {
|
|
1906
1972
|
throw new BagSerializationError(`Missing or undecodable TYTX parent branch: ${parentPath}`);
|
|
1907
1973
|
}
|
|
1908
|
-
const parentBag = pathToBag.get(
|
|
1909
|
-
const fullPath = parentPath
|
|
1974
|
+
const parentBag = pathToBag.get(parentKey);
|
|
1975
|
+
const fullPath = JSON.stringify([...parentPath, label]);
|
|
1910
1976
|
let value = rawValue;
|
|
1911
|
-
// MessagePack does not rescan text. Only Bag-owned empty markers
|
|
1912
|
-
// are structural: arbitrary scalar decoders must never run here.
|
|
1913
|
-
if (transport === 'msgpack' && typeof value === 'string' && value.startsWith('::')) {
|
|
1914
|
-
const cls = getRegisteredType(value.slice(2));
|
|
1915
|
-
if (cls === Bag || cls?.prototype instanceof Bag) value = tytxDecode(value);
|
|
1916
|
-
}
|
|
1917
1977
|
const resolver = decodeResolver(value);
|
|
1918
1978
|
const attr = decodeAttrs(rawAttr);
|
|
1919
1979
|
const branchCls = Object.hasOwn(attr, CLS_ATTRIBUTE) ? attr[CLS_ATTRIBUTE] : null;
|
|
@@ -1929,10 +1989,8 @@ export class Bag {
|
|
|
1929
1989
|
} else if (branchCls !== null) {
|
|
1930
1990
|
throw new BagSerializationError(
|
|
1931
1991
|
`node '${fullPath}': the attribute '${CLS_ATTRIBUTE}' is reserved for the Bag class name of a branch`);
|
|
1932
|
-
} else if (value === '::NN') {
|
|
1933
|
-
value = null;
|
|
1934
1992
|
}
|
|
1935
|
-
const node = parentBag.
|
|
1993
|
+
const node = parentBag._appendLiteral(label, resolver ? null : value, attr, resolver);
|
|
1936
1994
|
node.nodeTag = tag ?? null;
|
|
1937
1995
|
}
|
|
1938
1996
|
return bag;
|
|
@@ -2198,7 +2256,7 @@ export class Bag {
|
|
|
2198
2256
|
const text = await response.text();
|
|
2199
2257
|
|
|
2200
2258
|
if (contentType.includes('json')) {
|
|
2201
|
-
return Bag.
|
|
2259
|
+
return Bag.fromTytx(text);
|
|
2202
2260
|
}
|
|
2203
2261
|
if (contentType.includes('xml')) {
|
|
2204
2262
|
return Bag.fromXml(text);
|
|
@@ -2209,7 +2267,7 @@ export class Bag {
|
|
|
2209
2267
|
return Bag.fromXml(text);
|
|
2210
2268
|
}
|
|
2211
2269
|
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
|
2212
|
-
return Bag.
|
|
2270
|
+
return Bag.fromTytx(text);
|
|
2213
2271
|
}
|
|
2214
2272
|
throw new BagException(`Unsupported content-type: ${contentType}`);
|
|
2215
2273
|
}
|
|
@@ -2262,79 +2320,17 @@ export class Bag {
|
|
|
2262
2320
|
}
|
|
2263
2321
|
|
|
2264
2322
|
/**
|
|
2265
|
-
*
|
|
2266
|
-
*
|
|
2267
|
-
*
|
|
2268
|
-
*
|
|
2269
|
-
*
|
|
2270
|
-
* @param {string|Object|Array} source - JSON string, dict or list to parse.
|
|
2271
|
-
* @param {string} [listJoiner=null] - If provided, join string arrays with this separator.
|
|
2272
|
-
* @returns {Bag} Deserialized Bag.
|
|
2323
|
+
* Deprecated alias of fromTytx: serialized text only, no node heuristics.
|
|
2324
|
+
* @deprecated Use fromTytx, or new Bag(data) for native dictionaries/arrays.
|
|
2325
|
+
* @param {string|Uint8Array} source
|
|
2326
|
+
* @param {{recursive?: boolean}} [options={}]
|
|
2273
2327
|
*/
|
|
2274
|
-
static fromJson(source,
|
|
2275
|
-
if (
|
|
2276
|
-
|
|
2328
|
+
static fromJson(source, options = {}) {
|
|
2329
|
+
if (!warnedFromJson) {
|
|
2330
|
+
console.warn('Bag.fromJson is deprecated; use Bag.fromTytx');
|
|
2331
|
+
warnedFromJson = true;
|
|
2277
2332
|
}
|
|
2278
|
-
|
|
2279
|
-
if (!Array.isArray(source) && typeof source !== 'object') {
|
|
2280
|
-
source = { value: source };
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
return Bag._fromJsonRecursive(source, listJoiner);
|
|
2284
|
-
}
|
|
2285
|
-
|
|
2286
|
-
/**
|
|
2287
|
-
* Recursively convert JSON data to Bag (internal).
|
|
2288
|
-
* @private
|
|
2289
|
-
*/
|
|
2290
|
-
static _fromJsonRecursive(data, listJoiner = null) {
|
|
2291
|
-
if (data instanceof Bag) return data;
|
|
2292
|
-
if (Array.isArray(data)) {
|
|
2293
|
-
// listJoiner: join string arrays into a single string
|
|
2294
|
-
if (listJoiner !== null && data.every(item => typeof item === 'string')) {
|
|
2295
|
-
return data.join(listJoiner);
|
|
2296
|
-
}
|
|
2297
|
-
|
|
2298
|
-
if (data.length === 0) {
|
|
2299
|
-
return new Bag();
|
|
2300
|
-
}
|
|
2301
|
-
|
|
2302
|
-
// Check if list items have 'label' key (Bag node format)
|
|
2303
|
-
if (typeof data[0] === 'object' && data[0] !== null && 'label' in data[0]) {
|
|
2304
|
-
const result = new Bag();
|
|
2305
|
-
for (const item of data) {
|
|
2306
|
-
const label = item.label;
|
|
2307
|
-
const value = Bag._fromJsonRecursive(item.value, listJoiner);
|
|
2308
|
-
const attr = decodeAttrs(item.attr);
|
|
2309
|
-
const node = result.setItem(label, value, attr);
|
|
2310
|
-
node.nodeTag = item.tag ?? null;
|
|
2311
|
-
const resolver = decodeResolver(item.resolver);
|
|
2312
|
-
if (resolver) node.resolver = resolver;
|
|
2313
|
-
}
|
|
2314
|
-
return result;
|
|
2315
|
-
}
|
|
2316
|
-
|
|
2317
|
-
// Generic list -> Bag with r_N keys
|
|
2318
|
-
const result = new Bag();
|
|
2319
|
-
for (let n = 0; n < data.length; n++) {
|
|
2320
|
-
result.setItem(`r_${n}`, Bag._fromJsonRecursive(data[n], listJoiner));
|
|
2321
|
-
}
|
|
2322
|
-
return result;
|
|
2323
|
-
}
|
|
2324
|
-
|
|
2325
|
-
if (typeof data === 'object' && data !== null) {
|
|
2326
|
-
if (Object.keys(data).length === 0) {
|
|
2327
|
-
return new Bag();
|
|
2328
|
-
}
|
|
2329
|
-
const result = new Bag();
|
|
2330
|
-
for (const [k, v] of Object.entries(data)) {
|
|
2331
|
-
result.setItem(k, Bag._fromJsonRecursive(v, listJoiner));
|
|
2332
|
-
}
|
|
2333
|
-
return result;
|
|
2334
|
-
}
|
|
2335
|
-
|
|
2336
|
-
// Scalar value
|
|
2337
|
-
return data;
|
|
2333
|
+
return this.fromTytx(source, 'json', options);
|
|
2338
2334
|
}
|
|
2339
2335
|
|
|
2340
2336
|
// -------------------------------------------------------------------------
|
|
@@ -2511,7 +2507,7 @@ export class BagException extends Error {
|
|
|
2511
2507
|
BagResolver.registerBagClass(Bag);
|
|
2512
2508
|
|
|
2513
2509
|
// Shared TYTX registry: subclasses register their own wire suffix explicitly.
|
|
2514
|
-
|
|
2510
|
+
registerType(Bag, 'X', bag => bag._toTytxPayload(), payload => Bag._fromTytxPayload(payload));
|
|
2515
2511
|
// Bag is the base class of the "X" type: its name must be in the subtype
|
|
2516
2512
|
// dictionary like every subclass that travels as "::X" with a __cls name.
|
|
2517
2513
|
setSubtypeDict(Bag.tytxSuffix, { ...getSubtypeDict(Bag.tytxSuffix), Bag });
|
package/src/index.d.ts
CHANGED
|
@@ -20,7 +20,11 @@ export const RETRY_POLICIES: {
|
|
|
20
20
|
|
|
21
21
|
/** Options shared by every resolver. */
|
|
22
22
|
export interface BagResolverOptions {
|
|
23
|
-
/**
|
|
23
|
+
/** Removed: schedule polling in the application. */
|
|
24
|
+
interval?: never;
|
|
25
|
+
/** Removed: invalidate and read explicitly. */
|
|
26
|
+
reactive?: never;
|
|
27
|
+
/** Cache duration in seconds. 0 disables the cache, a negative value never expires. */
|
|
24
28
|
cacheTime?: number;
|
|
25
29
|
/** If true, the resolved value is not stored in the node. */
|
|
26
30
|
readOnly?: boolean;
|
|
@@ -85,7 +89,8 @@ export class BagSerializationError extends Error {
|
|
|
85
89
|
export class Bag implements Iterable<BagNode> {
|
|
86
90
|
/** Wire suffix of the base class in the TYTX registry. */
|
|
87
91
|
static tytxSuffix: string;
|
|
88
|
-
constructor(source?: Record<string, any> |
|
|
92
|
+
constructor(source?: Record<string, any> | any[] | null, options?: { recursive?: boolean });
|
|
93
|
+
fillFrom(source?: Record<string, any> | any[] | null, options?: { recursive?: boolean }): this;
|
|
89
94
|
|
|
90
95
|
parent: Bag | null;
|
|
91
96
|
parentNode: BagNode | null;
|
|
@@ -158,12 +163,13 @@ export class Bag implements Iterable<BagNode> {
|
|
|
158
163
|
traverse(): Generator<BagNode>;
|
|
159
164
|
|
|
160
165
|
toTytx(transport?: 'json' | 'msgpack', compact?: boolean): string | Uint8Array;
|
|
161
|
-
static fromTytx(data: string | Uint8Array, transport?: 'json' | 'msgpack'): Bag;
|
|
166
|
+
static fromTytx(data: string | Uint8Array, transport?: 'json' | 'msgpack' | { recursive?: boolean }, options?: { recursive?: boolean }): Bag;
|
|
162
167
|
toXml(options?: ToXmlOptions): string;
|
|
163
168
|
static fromXml(source: string, options?: FromXmlOptions): Bag;
|
|
164
169
|
static fromUrl(url: string, options?: FromUrlOptions): Promise<Bag>;
|
|
165
170
|
toJson(typed?: boolean): string;
|
|
166
|
-
|
|
171
|
+
/** @deprecated Use fromTytx for text and the constructor for native data. */
|
|
172
|
+
static fromJson(source: string | Uint8Array, options?: { recursive?: boolean }): Bag;
|
|
167
173
|
toStringTree(isStatic?: boolean): string;
|
|
168
174
|
toString(isStatic?: boolean): string;
|
|
169
175
|
}
|
|
@@ -240,6 +246,7 @@ export class BagResolver {
|
|
|
240
246
|
init(): void;
|
|
241
247
|
setNode(node: BagNode | null): void;
|
|
242
248
|
onSetResolver(node: BagNode): void;
|
|
249
|
+
/** Invalidate the cache; reload only on the next explicit read. No refresh option. */
|
|
243
250
|
reset(): void;
|
|
244
251
|
resolve(options?: { static?: boolean; [key: string]: any }): any;
|
|
245
252
|
load(kwargs?: Record<string, any>): any;
|
package/src/resolver.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* - cacheTime = 0: never cache, always resolve
|
|
12
12
|
* - cacheTime < 0: cache forever (resolve once)
|
|
13
13
|
*
|
|
14
|
+
* Resolvers never schedule background loads. reset() reloads on the next read.
|
|
15
|
+
*
|
|
14
16
|
* Retry Policy:
|
|
15
17
|
* - retryPolicy = null -> NO retry (default)
|
|
16
18
|
* - retryPolicy = 'network' -> use predefined RETRY_POLICIES.network
|
|
@@ -160,6 +162,11 @@ export class BagResolver {
|
|
|
160
162
|
constructor(kwargs = {}) {
|
|
161
163
|
const defaults = this.constructor.classKwargs;
|
|
162
164
|
const merged = { ...defaults, ...kwargs };
|
|
165
|
+
for (const option of ['interval', 'reactive']) {
|
|
166
|
+
if (Object.hasOwn(merged, option)) {
|
|
167
|
+
throw new TypeError(`Removed resolver option: ${option}. Schedule updates in the application; use reset() and read explicitly.`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
163
170
|
|
|
164
171
|
// Extract internal params
|
|
165
172
|
this.cacheTime = merged.cacheTime;
|
|
@@ -282,7 +289,10 @@ export class BagResolver {
|
|
|
282
289
|
/**
|
|
283
290
|
* Reset the cache, forcing next resolve to call load().
|
|
284
291
|
*/
|
|
285
|
-
reset() {
|
|
292
|
+
reset(...args) {
|
|
293
|
+
if (args.length) {
|
|
294
|
+
throw new TypeError('reset() takes no arguments. It invalidates the cache for the next explicit read.');
|
|
295
|
+
}
|
|
286
296
|
this._lastUpdate = null;
|
|
287
297
|
this._lastEffectiveFingerprint = null;
|
|
288
298
|
}
|
|
@@ -342,7 +352,7 @@ export class BagResolver {
|
|
|
342
352
|
if (this._node && this._node.attr) {
|
|
343
353
|
const internalParams = this.constructor.internalParams;
|
|
344
354
|
for (const [key, value] of Object.entries(this._node.attr)) {
|
|
345
|
-
if (!internalParams.has(key)
|
|
355
|
+
if (!internalParams.has(key)) {
|
|
346
356
|
kwargs[key] = value;
|
|
347
357
|
}
|
|
348
358
|
}
|
|
@@ -435,8 +445,8 @@ export class BagResolver {
|
|
|
435
445
|
if (result.startsWith('<') && BagClass.fromXml) {
|
|
436
446
|
return BagClass.fromXml(result);
|
|
437
447
|
}
|
|
438
|
-
if ((result.startsWith('{') || result.startsWith('[')) && BagClass.
|
|
439
|
-
return BagClass.
|
|
448
|
+
if ((result.startsWith('{') || result.startsWith('[')) && BagClass.fromTytx) {
|
|
449
|
+
return BagClass.fromTytx(result);
|
|
440
450
|
}
|
|
441
451
|
}
|
|
442
452
|
return result;
|