resurrect-esm 2.0.5 → 2.0.7
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/.vscode/settings.json +1 -0
- package/README.md +34 -66
- package/package.json +8 -9
- package/resurrect.test.ts +12 -11
- package/resurrect.ts +164 -233
- package/tsconfig.json +3 -0
package/.vscode/settings.json
CHANGED
package/README.md
CHANGED
|
@@ -5,11 +5,11 @@ An ES6 module port of ResurrectTS.
|
|
|
5
5
|
> [!CAUTION]
|
|
6
6
|
> This is *not* just a naive `exports.foo` => `export {foo}` rewrite. Some API changes have been made:
|
|
7
7
|
>
|
|
8
|
-
> * All of the "irrelevant" methods of Resurrect have been renamed using private names
|
|
9
|
-
> * ResurrectError is now a top-level export, instead of living inside a Resurrect object.
|
|
10
|
-
> * NamespaceResolver is now a top-level export, instead of living inside the Resurrect static constructor namespace.
|
|
11
|
-
> * NamespaceResolver now has a new method, `getConstructor(name: string)`, which should return the constructor function to generate the object (
|
|
12
|
-
> * The bug found in skeeto/resurrect-js#11 has been fixed.
|
|
8
|
+
> * All of the "irrelevant" methods of Resurrect have been renamed using private `#names`.
|
|
9
|
+
> * `ResurrectError` is now a top-level export, instead of living inside a `Resurrect` object.
|
|
10
|
+
> * `NamespaceResolver` is now a top-level export, instead of living inside the `Resurrect` static constructor namespace.
|
|
11
|
+
> * `NamespaceResolver` now has a new method, `getConstructor(name: string)`, which should return the constructor function to generate the object (the constructor itself will not be called directly, so the parameters are irrelevant).
|
|
12
|
+
> * The bug found in [skeeto/resurrect-js#11][issue11] has been fixed.
|
|
13
13
|
|
|
14
14
|
ResurrectJS preserves object behavior (prototypes) and reference
|
|
15
15
|
circularity with a special JSON encoding. Unlike flat JSON, it can
|
|
@@ -62,30 +62,13 @@ const date = necromancer.resurrect(json);
|
|
|
62
62
|
Options are provided to the constructor as an object with these
|
|
63
63
|
properties:
|
|
64
64
|
|
|
65
|
-
* *prefix* (`"#"`): A prefix string used for temporary properties added
|
|
66
|
-
to objects during serialization and deserialization. It is
|
|
67
|
-
important that you don't use any properties beginning with this
|
|
68
|
-
string. This option must be consistent between both serialization
|
|
69
|
-
and deserialization.
|
|
65
|
+
* *prefix* (default: `"#"`): A prefix string used for temporary properties added to objects during serialization and deserialization. It is important that you don't use any properties beginning with this string. This option also must be the same between both serialization and deserialization.
|
|
70
66
|
|
|
71
|
-
* *cleanup* (
|
|
72
|
-
serialization and deserialization using the `delete` operator.
|
|
73
|
-
This may cause performance penalties (i.e. breaking hidden
|
|
74
|
-
classes in V8) on objects that ResurrectJS touches, so enable
|
|
75
|
-
with care.
|
|
67
|
+
* *cleanup* (default: false): Perform full property cleanup after both serialization and deserialization using the `delete` operator. This may cause performance penalties (i.e. breaking hidden classes and forcing de-optimization) on objects that Resurrect touches, so enable with care.
|
|
76
68
|
|
|
77
|
-
* *revive* (
|
|
78
|
-
have been resurrected. If this is set to false during
|
|
79
|
-
serialization, resurrection information will not be encoded. You
|
|
80
|
-
still get circularity and Date support.
|
|
69
|
+
* *revive* (default: true): Restore behavior (`__proto__`) to objects that have been resurrected. If this is set to false during serialization, resurrection information will not be encoded. You still get circularity and Date/HTML/URL support.
|
|
81
70
|
|
|
82
|
-
* *resolver* (`NamespaceResolver
|
|
83
|
-
and a prototype. Create a custom resolver if your constructors
|
|
84
|
-
are not stored in global variables. The resolver has three methods:
|
|
85
|
-
getName(object), getConstructor(string), and getPrototype(string).
|
|
86
|
-
|
|
87
|
-
> [!CAUTION]
|
|
88
|
-
> If you're using ES6 modules for your custom classes, you MUST use a custom resolver since module scope is not global scope!
|
|
71
|
+
* *resolver* (`NamespaceResolver`, default undefined): Converts between a name and a prototype. Create a custom resolver if your constructors are not stored in global variables. The resolver has three methods: `getName(object)`, `getConstructor(string)`, and `getPrototype(string)`.
|
|
89
72
|
|
|
90
73
|
For example,
|
|
91
74
|
|
|
@@ -99,66 +82,51 @@ const necromancer = new Resurrect({
|
|
|
99
82
|
|
|
100
83
|
## Methods
|
|
101
84
|
|
|
102
|
-
|
|
85
|
+
There are only two public methods:
|
|
103
86
|
|
|
104
|
-
* `.stringify(object[, replacer[, space]])`: Serializes an arbitrary
|
|
105
|
-
object or value into a string. The `replacer` and `space`
|
|
106
|
-
arguments are the same as [JSON.stringify][json-mdn], being
|
|
107
|
-
passed through to this method. Note that the replacer will *not*
|
|
108
|
-
be called for ResurrectJS's intrusive keys.
|
|
87
|
+
* `.stringify(object[, replacer[, space]])`: Serializes an arbitrary object or value into a string. The `replacer` and `space` arguments are the same as [JSON.stringify][json-mdn], being passed through to this method. Note that the replacer will *not* be called for Resurrect's intrusive keys (i.e. the ones that are created using the `.prefix` option).
|
|
109
88
|
|
|
110
|
-
* `.resurrect(string)`: Deserializes an object stored in a string by
|
|
111
|
-
a previous call to `.stringify()`. Circularity and, optionally,
|
|
112
|
-
behavior (prototype chain) will be restored.
|
|
89
|
+
* `.resurrect(string)`: Deserializes an object stored in a string by a previous call to `.stringify()`. Circularity and, optionally, behavior (prototype chain) will be restored.
|
|
113
90
|
|
|
114
91
|
## Restrictions
|
|
115
92
|
|
|
116
|
-
With the default resolver, all constructors must be named and stored
|
|
117
|
-
|
|
118
|
-
|
|
93
|
+
With the default resolver, all constructors must be named and stored in the global scope under that name. This is required so that the prototypes can be looked up and reconnected at resurrection time.
|
|
94
|
+
|
|
95
|
+
> [!CAUTION]
|
|
96
|
+
> If you're using ES6 modules for your custom classes, you MUST use a custom resolver since module scope is not global scope!
|
|
119
97
|
|
|
120
|
-
The wrapper objects Boolean, String, and Number will be
|
|
121
|
-
unwrapped. This means extra properties added to these objects will not
|
|
122
|
-
be preserved.
|
|
98
|
+
The wrapper objects Boolean, String, and Number will be unwrapped. This means extra properties added to these objects will not be preserved.
|
|
123
99
|
|
|
124
|
-
Functions cannot ever be serialized. Resurrect will throw an error if
|
|
125
|
-
a function is found when traversing a data structure, rather than just silently dropping the property or replacing it with `null` like JSON.stringify does.
|
|
100
|
+
Functions cannot ever be serialized. Resurrect will throw an error if a function is found when traversing a data structure, rather than just silently dropping the property or replacing it with `null` like JSON.stringify does.
|
|
126
101
|
|
|
127
102
|
### Custom Resolvers
|
|
128
103
|
|
|
129
|
-
|
|
130
|
-
constructors *must* be explicitly named when defined. For example, see
|
|
131
|
-
the Foo constructor in this example,
|
|
104
|
+
New in 2.0.5: the NamespaceResolver will now use the name on the namespace object of whatever you pass in instead of the `.name` of the object's constructor. This is to better support minifiers that rename the class and produce code output similar to this:
|
|
132
105
|
|
|
133
106
|
```ts
|
|
134
|
-
import { Resurrect, NamespaceResolver } from "resurrect-esm";
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
107
|
+
import { Resurrect as r, NamespaceResolver as e } from "resurrect-esm";
|
|
108
|
+
var c = class d {
|
|
109
|
+
constructor() {
|
|
110
|
+
this.bar = true;
|
|
111
|
+
d.bax(this);
|
|
112
|
+
}
|
|
113
|
+
static bax(obj) {
|
|
114
|
+
|
|
144
115
|
}
|
|
145
116
|
};
|
|
146
|
-
|
|
147
|
-
|
|
117
|
+
var n = {
|
|
118
|
+
Foo: c, // <== Always uses name given here
|
|
119
|
+
};
|
|
120
|
+
var a = new r({
|
|
121
|
+
resolver: new e(n)
|
|
148
122
|
});
|
|
149
123
|
```
|
|
150
124
|
|
|
151
|
-
|
|
152
|
-
itself has been given a matching name. This is how the resolver will
|
|
153
|
-
find the name of the constructor in the namespace when given the
|
|
154
|
-
constructor. Keep in mind that using this form will bind the variable
|
|
155
|
-
Foo to the surrounding function within the body of Foo.
|
|
156
|
-
|
|
157
|
-
If you're using a bundler, you **must** enable the "keep names" option
|
|
158
|
-
for at least the classes that will be stringified, so that Resurrect.js can get the correct `.name` from the constructor functions. (For esbuild, the option is `--keep-names`.)
|
|
125
|
+
In the serializer above, the "name" given to the class will always be "Foo", even though the `.name` of the constructor is "c" and might be different on the next build.
|
|
159
126
|
|
|
160
127
|
## See Also
|
|
161
128
|
|
|
162
129
|
* [HydrateJS](https://github.com/nanodeath/HydrateJS)
|
|
163
130
|
|
|
164
131
|
[json-mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
|
|
132
|
+
[issue11]: https://github.com/skeeto/resurrect-js/issues/11
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "resurrect-esm",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "ResurrectJS preserves object behavior (prototypes) and reference circularity with a special JSON encoding. Unlike flat JSON, it can also properly resurrect self-referential objects, objects with shared structure, Dates, RegExps, HTMLElements, NaN, Infinity, undefined (which won't get turned into null), and any other custom object type given the proper resolver.",
|
|
6
6
|
"repository": {
|
|
@@ -20,24 +20,23 @@
|
|
|
20
20
|
"url": "https://github.com/dragoncoder047"
|
|
21
21
|
}
|
|
22
22
|
],
|
|
23
|
-
"license": "
|
|
23
|
+
"license": "CC0-1.0",
|
|
24
24
|
"bugs": {
|
|
25
25
|
"url": "https://github.com/dragoncoder047/resurrect-esm/issues"
|
|
26
26
|
},
|
|
27
27
|
"homepage": "https://github.com/dragoncoder047/resurrect-esm#readme",
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@happy-dom/global-registrator": "^20.
|
|
30
|
-
"@types/bun": "^1.3.
|
|
31
|
-
"bun": "^1.3.
|
|
32
|
-
"
|
|
33
|
-
"typescript": "^5.6.2"
|
|
29
|
+
"@happy-dom/global-registrator": "^20.10.5",
|
|
30
|
+
"@types/bun": "^1.3.14",
|
|
31
|
+
"bun": "^1.3.14",
|
|
32
|
+
"typescript": "^5.9.3"
|
|
34
33
|
},
|
|
35
34
|
"main": "resurrect.ts",
|
|
36
35
|
"scripts": {
|
|
37
|
-
"build": "pnpm
|
|
36
|
+
"build": "pnpm bun build --sourcemap --target=browser --format=esm resurrect.ts --outfile=resurrect.js",
|
|
38
37
|
"test": "pnpm bun test",
|
|
39
38
|
"test:watch": "pnpm test --watch",
|
|
40
|
-
"prepare": "pnpm build --minify
|
|
39
|
+
"prepare": "pnpm build --minify"
|
|
41
40
|
},
|
|
42
41
|
"dependencies": {
|
|
43
42
|
"lib0": "^0.2.117"
|
package/resurrect.test.ts
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { GlobalRegistrator } from "@happy-dom/global-registrator";
|
|
2
2
|
import { describe, expect, test } from "bun:test";
|
|
3
|
-
import { NamespaceResolver, Resurrect, ResurrectError, ResurrectOptions } from "./resurrect";
|
|
3
|
+
import { FakeNode, NamespaceResolver, Resurrect, ResurrectError, ResurrectOptions } from "./resurrect";
|
|
4
4
|
|
|
5
5
|
GlobalRegistrator.register();
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
describe.each([
|
|
8
|
+
["default options", {}],
|
|
9
|
+
["custom prefix", { prefix: "qwerty" }],
|
|
10
|
+
["no revive", { revive: false }]
|
|
11
|
+
])("%s", (_, opt: ResurrectOptions) => {
|
|
8
12
|
|
|
9
13
|
const defOpts = new Resurrect(opt);
|
|
10
14
|
|
|
@@ -20,8 +24,8 @@ function suite(opt?: ResurrectOptions) {
|
|
|
20
24
|
expect(roundtrip(undefined)).toBeUndefined();
|
|
21
25
|
expect(roundtrip("foo")).toBe("foo");
|
|
22
26
|
expect(roundtrip(NaN)).toBeNaN();
|
|
23
|
-
expect(roundtrip(Infinity)).
|
|
24
|
-
expect(roundtrip(-Infinity)).
|
|
27
|
+
expect(roundtrip(Infinity)).toEqual(Infinity);
|
|
28
|
+
expect(roundtrip(-Infinity)).toEqual(-Infinity);
|
|
25
29
|
});
|
|
26
30
|
|
|
27
31
|
test("basic JSON serialization", () => {
|
|
@@ -55,7 +59,7 @@ function suite(opt?: ResurrectOptions) {
|
|
|
55
59
|
});
|
|
56
60
|
|
|
57
61
|
test("serialization with DOM elements", () => {
|
|
58
|
-
const obj = new
|
|
62
|
+
const obj = new FakeNode("<span id=foo><a id=1></a></span>");
|
|
59
63
|
const roundtripped = roundtrip(obj);
|
|
60
64
|
expect(roundtripped).toBeInstanceOf(HTMLSpanElement);
|
|
61
65
|
expect(roundtripped.firstChild).toBeInstanceOf(HTMLAnchorElement);
|
|
@@ -121,21 +125,18 @@ function suite(opt?: ResurrectOptions) {
|
|
|
121
125
|
test("no revive preserves own properties but not functionality", () => {
|
|
122
126
|
class Dog {
|
|
123
127
|
constructor(public loudness: number, public sound: string) { }
|
|
124
|
-
woof() {
|
|
128
|
+
woof() { }
|
|
125
129
|
}
|
|
126
130
|
const obj = new Dog(3, "wow");
|
|
127
131
|
const roundtripped = roundtrip(obj, {
|
|
128
132
|
resolver: new NamespaceResolver({ Dog }),
|
|
129
133
|
});
|
|
130
134
|
expect(roundtripped).not.toBeInstanceOf(Dog);
|
|
135
|
+
expect(roundtripped).toEqual({ loudness: 3, sound: "wow" } as Dog);
|
|
131
136
|
expect(roundtripped.woof).toBeUndefined();
|
|
132
137
|
});
|
|
133
138
|
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
describe("default options", () => suite());
|
|
137
|
-
describe("custom prefix", () => suite({ prefix: "qwerty" }));
|
|
138
|
-
describe("no revive", () => suite({ revive: false }));
|
|
139
|
+
});
|
|
139
140
|
|
|
140
141
|
test("malformed state bug", () => {
|
|
141
142
|
// Test for the bug found in skeeto/resurrect-js#11
|
package/resurrect.ts
CHANGED
|
@@ -1,183 +1,114 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* @version
|
|
4
|
-
* @license
|
|
5
|
-
*
|
|
6
|
-
* ResurrectJS preserves object behavior (prototypes) and reference
|
|
7
|
-
* circularity with a special JSON encoding. Unlike regular JSON,
|
|
8
|
-
* Date, URL, RegExp, DOM objects, and `undefined` are also properly
|
|
9
|
-
* preserved.
|
|
10
|
-
*
|
|
11
|
-
* ## Examples
|
|
12
|
-
*
|
|
13
|
-
* function Foo() {}
|
|
14
|
-
* Foo.prototype.greet = function() { return "hello"; };
|
|
15
|
-
*
|
|
16
|
-
* // Behavior is preserved:
|
|
17
|
-
* const necromancer = new Resurrect();
|
|
18
|
-
* const json = necromancer.stringify(new Foo());
|
|
19
|
-
* const foo = necromancer.resurrect(json);
|
|
20
|
-
* foo.greet(); // => "hello"
|
|
21
|
-
*
|
|
22
|
-
* // References to the same object are preserved:
|
|
23
|
-
* json = necromancer.stringify([foo, foo]);
|
|
24
|
-
* const array = necromancer.resurrect(json);
|
|
25
|
-
* array[0] === array[1]; // => true
|
|
26
|
-
* array[1].greet(); // => "hello"
|
|
27
|
-
*
|
|
28
|
-
* // Dates are restored properly
|
|
29
|
-
* json = necromancer.stringify(new Date());
|
|
30
|
-
* const date = necromancer.resurrect(json);
|
|
31
|
-
* Object.prototype.toString.call(date); // => "[object Date]"
|
|
32
|
-
*
|
|
33
|
-
* ## Options
|
|
34
|
-
*
|
|
35
|
-
* Options are provided to the constructor as an object with these
|
|
36
|
-
* properties:
|
|
37
|
-
*
|
|
38
|
-
* prefix ('#'): A prefix string used for temporary properties added
|
|
39
|
-
* to objects during serialization and deserialization. It is
|
|
40
|
-
* important that you don't use any properties beginning with this
|
|
41
|
-
* string. This option must be consistent between both
|
|
42
|
-
* serialization and deserialization.
|
|
43
|
-
*
|
|
44
|
-
* revive (true): Restore behavior (__proto__) to objects that have
|
|
45
|
-
* been resurrected. If this is set to false during serialization,
|
|
46
|
-
* resurrection information will not be encoded. You still get
|
|
47
|
-
* circularity and Date/URL support.
|
|
48
|
-
*
|
|
49
|
-
* resolver (Resurrect.NamespaceResolver(window)): Converts between
|
|
50
|
-
* a name and a prototype. Create a custom resolver if your
|
|
51
|
-
* constructors are not stored in global variables. The resolver
|
|
52
|
-
* has three methods: getName(object), getConstructor(string), and
|
|
53
|
-
* getPrototype(string).
|
|
54
|
-
*
|
|
55
|
-
* For example,
|
|
56
|
-
*
|
|
57
|
-
* const necromancer = new Resurrect({
|
|
58
|
-
* prefix: "__#",
|
|
59
|
-
* });
|
|
60
|
-
*
|
|
61
|
-
* ## Caveats
|
|
62
|
-
*
|
|
63
|
-
* * With the default resolver, all constructors must be named and
|
|
64
|
-
* stored in the global variable under that name. This is required
|
|
65
|
-
* so that the prototypes can be looked up and reconnected at
|
|
66
|
-
* resurrection time.
|
|
67
|
-
*
|
|
68
|
-
* * The wrapper objects Boolean, String, and Number will be
|
|
69
|
-
* unwrapped. This means extra properties added to these objects
|
|
70
|
-
* will not be preserved.
|
|
71
|
-
*
|
|
72
|
-
* * Functions cannot ever be serialized. Resurrect will throw an
|
|
73
|
-
* error if a function is found when traversing a data structure.
|
|
74
|
-
*
|
|
2
|
+
* resurrect-esm, a fork of resurrect-ts, which is in turn a fork of resurrect.js
|
|
3
|
+
* @version 2.0.7
|
|
4
|
+
* @license CC0
|
|
75
5
|
* @see http://nullprogram.com/blog/2013/03/28/
|
|
76
6
|
*/
|
|
77
7
|
|
|
8
|
+
import { isArray } from "lib0/array";
|
|
9
|
+
import { isNumber, isString } from "lib0/function";
|
|
78
10
|
import { parse, stringify } from "lib0/json";
|
|
79
11
|
import { keys } from "lib0/object";
|
|
80
12
|
|
|
81
13
|
const getPrototypeOf = Object.getPrototypeOf;
|
|
14
|
+
const getOwnPropertyNames = Object.getOwnPropertyNames;
|
|
15
|
+
const create = Object.create;
|
|
16
|
+
/**
|
|
17
|
+
* Portable access to the global object (window, global).
|
|
18
|
+
* Uses indirect eval.
|
|
19
|
+
* @constant
|
|
20
|
+
*/
|
|
21
|
+
const GLOBAL: typeof globalThis = globalThis ?? (0, eval)("this");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Escape special regular expression characters in a string.
|
|
25
|
+
* Uses `RegExp.escape` if available, otherwise falls back to http://stackoverflow.com/a/6969486.
|
|
26
|
+
* @param {string} string
|
|
27
|
+
* @returns {string} The string escaped for exact matches.
|
|
28
|
+
*/
|
|
29
|
+
const escapeRegExp = RegExp.escape ?? ((string: string) => string.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"));
|
|
30
|
+
|
|
31
|
+
const is = (type: string) => {
|
|
32
|
+
const string = `[object ${type}]`;
|
|
33
|
+
return (obj: any) => {
|
|
34
|
+
return {}.toString.call(obj) === string;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const isBoolean = is("Boolean") as (obj: any) => obj is boolean;
|
|
39
|
+
const isFunction = is("Function") as (obj: any) => obj is Function;
|
|
40
|
+
const isDate = is("Date") as (obj: any) => obj is Date;
|
|
41
|
+
const isURL = is("URL") as (obj: any) => obj is URL;
|
|
42
|
+
const isRegExp = is("RegExp") as (obj: any) => obj is RegExp;
|
|
43
|
+
const isObject = is("Object") as (obj: any) => obj is object;
|
|
44
|
+
const isAtom = (object: any) => {
|
|
45
|
+
return !isObject(object) && !isArray(object);
|
|
46
|
+
}
|
|
47
|
+
const isPrimitive = (object: any) => {
|
|
48
|
+
return object == null ||
|
|
49
|
+
isNumber(object) ||
|
|
50
|
+
isString(object) ||
|
|
51
|
+
isBoolean(object);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Create a DOM node from HTML source; behaves like a constructor.
|
|
56
|
+
*/
|
|
57
|
+
export const FakeNode = class {
|
|
58
|
+
constructor(html: string) {
|
|
59
|
+
const div = document.createElement("a");
|
|
60
|
+
div.innerHTML = html;
|
|
61
|
+
return div.firstChild as HTMLElement;
|
|
62
|
+
}
|
|
63
|
+
} as new (x: string) => HTMLElement;
|
|
64
|
+
|
|
82
65
|
|
|
83
66
|
export class Resurrect {
|
|
84
|
-
|
|
85
|
-
|
|
67
|
+
#table: any[] | null;
|
|
68
|
+
#cleanups: (() => void)[] = [];
|
|
86
69
|
prefix: string;
|
|
87
70
|
cleanup: boolean;
|
|
88
71
|
revive: boolean;
|
|
89
|
-
get
|
|
90
|
-
get
|
|
91
|
-
get
|
|
92
|
-
get
|
|
93
|
-
get
|
|
94
|
-
get
|
|
72
|
+
get #refcode() { return this.prefix + "#" };
|
|
73
|
+
get #backrefcode() { return this.prefix + "=" };
|
|
74
|
+
get #protocode() { return this.prefix + "+" };
|
|
75
|
+
get #origcode() { return this.prefix + "&" };
|
|
76
|
+
get #buildcode() { return this.prefix + "@" };
|
|
77
|
+
get #valuecode() { return this.prefix + "_" };
|
|
95
78
|
resolver: NamespaceResolver;
|
|
96
79
|
constructor(opt: ResurrectOptions = {}) {
|
|
97
|
-
this
|
|
80
|
+
this.#table = null;
|
|
98
81
|
this.prefix = opt.prefix ?? "#";
|
|
99
82
|
this.cleanup = opt.cleanup ?? false;
|
|
100
83
|
this.revive = opt.revive ?? true;
|
|
101
|
-
this.resolver = opt.resolver ?? new NamespaceResolver(
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Portable access to the global object (window, global).
|
|
106
|
-
* Uses indirect eval.
|
|
107
|
-
* @constant
|
|
108
|
-
*/
|
|
109
|
-
static readonly GLOBAL: typeof globalThis = globalThis ?? (0, eval)("this");
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Escape special regular expression characters in a string.
|
|
113
|
-
* Uses `RegExp.escape` if available, otherwise falls back to http://stackoverflow.com/a/6969486.
|
|
114
|
-
* @param {string} string
|
|
115
|
-
* @returns {string} The string escaped for exact matches.
|
|
116
|
-
*/
|
|
117
|
-
private static _escapeRegExp = RegExp.escape ?? ((string: string) => string.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"));
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* Create a DOM node from HTML source; behaves like a constructor.
|
|
121
|
-
*/
|
|
122
|
-
static Node = class {
|
|
123
|
-
constructor(html: string) {
|
|
124
|
-
const div = document.createElement("a");
|
|
125
|
-
div.innerHTML = html;
|
|
126
|
-
return div.firstChild as HTMLElement;
|
|
127
|
-
}
|
|
128
|
-
} as new (x: string) => HTMLElement;
|
|
129
|
-
|
|
130
|
-
private static _is(type: string) {
|
|
131
|
-
const string = `[object ${type}]`;
|
|
132
|
-
return (obj: any) => {
|
|
133
|
-
return {}.toString.call(obj) === string;
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
private static _isArray = Resurrect._is("Array") as (obj: any) => obj is any[];
|
|
138
|
-
private static _isString = Resurrect._is("String") as (obj: any) => obj is string;
|
|
139
|
-
private static _isBoolean = Resurrect._is("Boolean") as (obj: any) => obj is boolean;
|
|
140
|
-
private static _isNumber = Resurrect._is("Number") as (obj: any) => obj is number;
|
|
141
|
-
private static _isFunction = Resurrect._is("Function") as (obj: any) => obj is Function;
|
|
142
|
-
private static _isDate = Resurrect._is("Date") as (obj: any) => obj is Date;
|
|
143
|
-
private static _isURL = Resurrect._is("URL") as (obj: any) => obj is URL;
|
|
144
|
-
private static _isRegExp = Resurrect._is("RegExp") as (obj: any) => obj is RegExp;
|
|
145
|
-
private static _isObject = Resurrect._is("Object") as (obj: any) => obj is object;
|
|
146
|
-
private static _isAtom(object: any) {
|
|
147
|
-
return !Resurrect._isObject(object) && !Resurrect._isArray(object);
|
|
148
|
-
}
|
|
149
|
-
private static _isPrimitive(object: any) {
|
|
150
|
-
return object == null ||
|
|
151
|
-
Resurrect._isNumber(object) ||
|
|
152
|
-
Resurrect._isString(object) ||
|
|
153
|
-
Resurrect._isBoolean(object);
|
|
84
|
+
this.resolver = opt.resolver ?? new NamespaceResolver(GLOBAL as any);
|
|
154
85
|
}
|
|
155
86
|
|
|
156
|
-
|
|
157
|
-
this.
|
|
158
|
-
this
|
|
87
|
+
#cleanup() {
|
|
88
|
+
this.#cleanups.forEach(e => e());
|
|
89
|
+
this.#cleanups = [];
|
|
159
90
|
}
|
|
160
91
|
|
|
161
92
|
/**
|
|
162
93
|
* Create a reference (encoding) to an object.
|
|
163
94
|
*/
|
|
164
|
-
|
|
95
|
+
#reference(object: any) {
|
|
165
96
|
return {
|
|
166
|
-
[this
|
|
97
|
+
[this.#backrefcode]: object === undefined ? -1 : object[this.#refcode],
|
|
167
98
|
};
|
|
168
99
|
}
|
|
169
100
|
|
|
170
101
|
/**
|
|
171
102
|
* Lookup an object in the table by reference object.
|
|
172
103
|
*/
|
|
173
|
-
|
|
174
|
-
return this
|
|
104
|
+
#dereference(ref: any) {
|
|
105
|
+
return this.#table![ref[this.#backrefcode]];
|
|
175
106
|
}
|
|
176
107
|
|
|
177
108
|
/**
|
|
178
109
|
* Put a temporary identifier on an object and store it in the table.
|
|
179
110
|
*/
|
|
180
|
-
|
|
111
|
+
#tagObject(object: any) {
|
|
181
112
|
if (this.revive) {
|
|
182
113
|
const constructor = this.resolver.getName(object);
|
|
183
114
|
if (constructor) {
|
|
@@ -185,25 +116,25 @@ export class Resurrect {
|
|
|
185
116
|
if (this.resolver.getPrototype(constructor) !== proto) {
|
|
186
117
|
throw new ResurrectError("Constructor mismatch!");
|
|
187
118
|
} else {
|
|
188
|
-
object[this
|
|
189
|
-
this.
|
|
119
|
+
object[this.#protocode] = constructor;
|
|
120
|
+
this.#cleanups.push(() => delete object[this.#protocode]);
|
|
190
121
|
}
|
|
191
122
|
}
|
|
192
123
|
}
|
|
193
|
-
object[this
|
|
194
|
-
this.
|
|
195
|
-
this
|
|
196
|
-
return object[this
|
|
124
|
+
object[this.#refcode] = this.#table!.length;
|
|
125
|
+
this.#cleanups.push(() => delete object[this.#refcode]);
|
|
126
|
+
this.#table!.push(object);
|
|
127
|
+
return object[this.#refcode];
|
|
197
128
|
}
|
|
198
129
|
|
|
199
130
|
/**
|
|
200
131
|
* Create a builder object (encoding) for serialization.
|
|
201
132
|
* @param value The value to pass to the constructor.
|
|
202
133
|
*/
|
|
203
|
-
|
|
134
|
+
#createBuilderObject(name: string, value: any) {
|
|
204
135
|
return {
|
|
205
|
-
[this
|
|
206
|
-
[this
|
|
136
|
+
[this.#buildcode]: name,
|
|
137
|
+
[this.#valuecode]: value
|
|
207
138
|
};
|
|
208
139
|
}
|
|
209
140
|
|
|
@@ -212,11 +143,11 @@ export class Resurrect {
|
|
|
212
143
|
* @see http://stackoverflow.com/a/14378462
|
|
213
144
|
* @see http://nullprogram.com/blog/2013/03/24/
|
|
214
145
|
*/
|
|
215
|
-
|
|
216
|
-
const type = this.resolver.getConstructor(ref[this
|
|
146
|
+
#buildFromEncoding(ref: any) {
|
|
147
|
+
const type = this.resolver.getConstructor(ref[this.#buildcode]);
|
|
217
148
|
/* Brilliant hack by kybernetikos: */
|
|
218
|
-
const result: any = new (type.bind.apply(type, [null].concat(ref[this
|
|
219
|
-
if (
|
|
149
|
+
const result: any = new (type.bind.apply(type, [null].concat(ref[this.#valuecode]) as [any, any[]]))();
|
|
150
|
+
if (isPrimitive(result)) {
|
|
220
151
|
return result.valueOf(); // unwrap
|
|
221
152
|
} else {
|
|
222
153
|
return result;
|
|
@@ -227,11 +158,11 @@ export class Resurrect {
|
|
|
227
158
|
* Dereference or build an object or value from an encoding.
|
|
228
159
|
* @method
|
|
229
160
|
*/
|
|
230
|
-
|
|
231
|
-
if (this
|
|
232
|
-
return this
|
|
233
|
-
} else if (this
|
|
234
|
-
return this
|
|
161
|
+
#decodeReference(ref: object) {
|
|
162
|
+
if (this.#backrefcode in ref) {
|
|
163
|
+
return this.#dereference(ref);
|
|
164
|
+
} else if (this.#buildcode in ref) {
|
|
165
|
+
return this.#buildFromEncoding(ref);
|
|
235
166
|
} else {
|
|
236
167
|
throw new ResurrectError("Unknown encoding.");
|
|
237
168
|
}
|
|
@@ -240,8 +171,8 @@ export class Resurrect {
|
|
|
240
171
|
/**
|
|
241
172
|
* @returns {boolean} True if the provided object is tagged for serialization.
|
|
242
173
|
*/
|
|
243
|
-
|
|
244
|
-
return (this
|
|
174
|
+
#isTagged(object: any): boolean {
|
|
175
|
+
return (this.#refcode in object) && (object[this.#refcode] != null);
|
|
245
176
|
}
|
|
246
177
|
|
|
247
178
|
|
|
@@ -249,58 +180,58 @@ export class Resurrect {
|
|
|
249
180
|
* Visit root and all its ancestors, visiting atoms with f.
|
|
250
181
|
* @returns A fresh copy of root to be serialized.
|
|
251
182
|
*/
|
|
252
|
-
|
|
253
|
-
if (
|
|
183
|
+
#visit(root: any, transform: (obj: any) => any, replacer?: (k: string, v: any) => any) {
|
|
184
|
+
if (isAtom(root)) {
|
|
254
185
|
return transform(root);
|
|
255
|
-
} else if (!this
|
|
186
|
+
} else if (!this.#isTagged(root)) {
|
|
256
187
|
let copy: any = null;
|
|
257
|
-
if (
|
|
188
|
+
if (isArray(root)) {
|
|
258
189
|
copy = [];
|
|
259
|
-
root[this
|
|
260
|
-
this.
|
|
190
|
+
root[this.#refcode as any] = this.#tagObject(copy);
|
|
191
|
+
this.#cleanups.push(() => delete root[this.#refcode as any]);
|
|
261
192
|
for (let i = 0; i < root.length; i++) {
|
|
262
|
-
copy.push(this
|
|
193
|
+
copy.push(this.#visit(root[i], transform, replacer));
|
|
263
194
|
}
|
|
264
195
|
} else { /* Object */
|
|
265
|
-
copy =
|
|
266
|
-
root[this
|
|
267
|
-
this.
|
|
268
|
-
for (const key of
|
|
196
|
+
copy = create(getPrototypeOf(root));
|
|
197
|
+
root[this.#refcode as any] = this.#tagObject(copy);
|
|
198
|
+
this.#cleanups.push(() => delete root[this.#refcode]);
|
|
199
|
+
for (const key of getOwnPropertyNames(root)) {
|
|
269
200
|
let value = root[key];
|
|
270
201
|
if (replacer && value !== undefined) {
|
|
271
202
|
// Call replacer like JSON.stringify's replacer
|
|
272
203
|
value = replacer.call(root, key, root[key]);
|
|
273
204
|
if (value === undefined) continue; // Omit from result
|
|
274
205
|
}
|
|
275
|
-
copy[key] = this
|
|
206
|
+
copy[key] = this.#visit(value, transform, replacer);
|
|
276
207
|
}
|
|
277
208
|
}
|
|
278
|
-
copy[this
|
|
279
|
-
return this
|
|
209
|
+
copy[this.#origcode] = root;
|
|
210
|
+
return this.#reference(copy);
|
|
280
211
|
} else {
|
|
281
|
-
return this
|
|
212
|
+
return this.#reference(root);
|
|
282
213
|
}
|
|
283
214
|
}
|
|
284
215
|
|
|
285
216
|
/**
|
|
286
217
|
* Manage special atom values, possibly returning an encoding.
|
|
287
218
|
*/
|
|
288
|
-
|
|
289
|
-
const Node =
|
|
290
|
-
if (
|
|
219
|
+
#encodeAtom(atom: any): any {
|
|
220
|
+
const Node = GLOBAL.Node || class { };
|
|
221
|
+
if (isFunction(atom)) {
|
|
291
222
|
throw new ResurrectError("Can't serialize functions.");
|
|
292
223
|
} else if (atom instanceof Node) {
|
|
293
|
-
return this
|
|
294
|
-
} else if (
|
|
295
|
-
return this
|
|
296
|
-
} else if (
|
|
297
|
-
return this
|
|
298
|
-
} else if (
|
|
299
|
-
return this
|
|
224
|
+
return this.#createBuilderObject("Resurrect.Node", [new XMLSerializer().serializeToString(atom)]);
|
|
225
|
+
} else if (isDate(atom)) {
|
|
226
|
+
return this.#createBuilderObject("Date", [atom.toISOString()]);
|
|
227
|
+
} else if (isURL(atom)) {
|
|
228
|
+
return this.#createBuilderObject("URL", [atom.href]);
|
|
229
|
+
} else if (isRegExp(atom)) {
|
|
230
|
+
return this.#createBuilderObject("RegExp", ("" + atom).match(/\/(.+)\/([a-z]*)/)!.slice(1));
|
|
300
231
|
} else if (atom === undefined) {
|
|
301
|
-
return this
|
|
302
|
-
} else if (
|
|
303
|
-
return this
|
|
232
|
+
return this.#reference(undefined);
|
|
233
|
+
} else if (isNumber(atom) && (isNaN(atom) || !isFinite(atom))) {
|
|
234
|
+
return this.#createBuilderObject("Number", ["" + atom]);
|
|
304
235
|
} else {
|
|
305
236
|
return atom;
|
|
306
237
|
}
|
|
@@ -310,8 +241,8 @@ export class Resurrect {
|
|
|
310
241
|
* Hides intrusive keys from a user-supplied replacer.
|
|
311
242
|
* @method
|
|
312
243
|
*/
|
|
313
|
-
|
|
314
|
-
const skip = new RegExp("^" +
|
|
244
|
+
#replacerWrapper<K extends string, V, U>(replacer: (k: K, v: V) => U): (k: K, v: V) => U | V {
|
|
245
|
+
const skip = new RegExp("^" + escapeRegExp(this.prefix));
|
|
315
246
|
return (k, v) => {
|
|
316
247
|
if (skip.test(k)) {
|
|
317
248
|
return v;
|
|
@@ -325,37 +256,37 @@ export class Resurrect {
|
|
|
325
256
|
* Serialize an arbitrary JavaScript object, carefully preserving it.
|
|
326
257
|
*/
|
|
327
258
|
stringify(object: any, replacer?: any[] | ((k: string, v: any) => any), space?: string | number) {
|
|
328
|
-
if (
|
|
329
|
-
replacer = this
|
|
330
|
-
} else if (
|
|
259
|
+
if (isFunction(replacer)) {
|
|
260
|
+
replacer = this.#replacerWrapper(replacer);
|
|
261
|
+
} else if (isArray(replacer)) {
|
|
331
262
|
const acceptKeys = replacer;
|
|
332
263
|
replacer = (k, v) => acceptKeys.includes(k) ? v : undefined;
|
|
333
264
|
}
|
|
334
|
-
if (
|
|
335
|
-
return stringify(this
|
|
265
|
+
if (isAtom(object)) {
|
|
266
|
+
return stringify(this.#encodeAtom(object), replacer, space);
|
|
336
267
|
} else {
|
|
337
|
-
this
|
|
338
|
-
const table = this
|
|
268
|
+
this.#cleanups = [];
|
|
269
|
+
const table = this.#table = [] as any[];
|
|
339
270
|
try {
|
|
340
|
-
this
|
|
271
|
+
this.#visit(object, this.#encodeAtom.bind(this), replacer);
|
|
341
272
|
} catch (e) {
|
|
342
|
-
this
|
|
273
|
+
this.#cleanup();
|
|
343
274
|
throw e;
|
|
344
275
|
} finally {
|
|
345
276
|
for (let i = 0; i < table.length; i++) {
|
|
346
277
|
if (this.cleanup) {
|
|
347
|
-
delete table[i]?.[this
|
|
278
|
+
delete table[i]?.[this.#origcode]?.[this.#refcode];
|
|
348
279
|
} else {
|
|
349
|
-
const obj = table[i]?.[this
|
|
350
|
-
if (obj) obj[this
|
|
280
|
+
const obj = table[i]?.[this.#origcode];
|
|
281
|
+
if (obj) obj[this.#refcode] = null;
|
|
351
282
|
}
|
|
352
|
-
delete table[i]?.[this
|
|
353
|
-
delete table[i]?.[this
|
|
283
|
+
delete table[i]?.[this.#refcode];
|
|
284
|
+
delete table[i]?.[this.#origcode];
|
|
354
285
|
}
|
|
355
|
-
this
|
|
286
|
+
this.#table = null;
|
|
356
287
|
}
|
|
357
288
|
const s = stringify(table, null, space);
|
|
358
|
-
if (this.cleanup) this
|
|
289
|
+
if (this.cleanup) this.#cleanup();
|
|
359
290
|
return s;
|
|
360
291
|
}
|
|
361
292
|
}
|
|
@@ -364,21 +295,21 @@ export class Resurrect {
|
|
|
364
295
|
* Restore the `__proto__` of the given object to the proper value.
|
|
365
296
|
* @method
|
|
366
297
|
*/
|
|
367
|
-
|
|
368
|
-
if (this
|
|
369
|
-
const name = (object as any)[this
|
|
298
|
+
#fixPrototype<T extends object>(object: T): T {
|
|
299
|
+
if (this.#protocode in object) {
|
|
300
|
+
const name = (object as any)[this.#protocode];
|
|
370
301
|
const prototype = this.resolver.getPrototype(name);
|
|
371
302
|
if ("__proto__" in object) {
|
|
372
303
|
object.__proto__ = prototype;
|
|
373
304
|
if (this.cleanup) {
|
|
374
|
-
delete (object as any)[this
|
|
305
|
+
delete (object as any)[this.#protocode];
|
|
375
306
|
}
|
|
376
307
|
return object;
|
|
377
308
|
}
|
|
378
|
-
// IE
|
|
379
|
-
const copy =
|
|
380
|
-
for (const key of
|
|
381
|
-
if (key !== this
|
|
309
|
+
// IE. Who uses this anyway...
|
|
310
|
+
const copy = create(prototype);
|
|
311
|
+
for (const key of getOwnPropertyNames(object)) {
|
|
312
|
+
if (key !== this.#protocode) {
|
|
382
313
|
copy[key] = (object as any)[key];
|
|
383
314
|
}
|
|
384
315
|
}
|
|
@@ -394,32 +325,32 @@ export class Resurrect {
|
|
|
394
325
|
let result = null;
|
|
395
326
|
const data = parse(string);
|
|
396
327
|
try {
|
|
397
|
-
if (
|
|
398
|
-
this
|
|
328
|
+
if (isArray(data)) {
|
|
329
|
+
this.#table = data;
|
|
399
330
|
/* Restore __proto__. */
|
|
400
331
|
if (this.revive) {
|
|
401
|
-
for (let i = 0; i < this.
|
|
402
|
-
this
|
|
332
|
+
for (let i = 0; i < this.#table.length; i++) {
|
|
333
|
+
this.#table[i] = this.#fixPrototype(this.#table[i]);
|
|
403
334
|
}
|
|
404
335
|
}
|
|
405
336
|
/* Re-establish object references and construct atoms. */
|
|
406
|
-
for (let i = 0; i < this.
|
|
407
|
-
const object = this
|
|
408
|
-
for (const key of
|
|
409
|
-
if (!(
|
|
410
|
-
object[key] = this
|
|
337
|
+
for (let i = 0; i < this.#table.length; i++) {
|
|
338
|
+
const object = this.#table[i];
|
|
339
|
+
for (const key of getOwnPropertyNames(object)) {
|
|
340
|
+
if (!(isAtom(object[key]))) {
|
|
341
|
+
object[key] = this.#decodeReference(object[key]);
|
|
411
342
|
}
|
|
412
343
|
}
|
|
413
344
|
}
|
|
414
|
-
result = this
|
|
415
|
-
} else if (
|
|
416
|
-
this
|
|
417
|
-
result = this
|
|
345
|
+
result = this.#table[0];
|
|
346
|
+
} else if (isObject(data)) {
|
|
347
|
+
this.#table = [];
|
|
348
|
+
result = this.#decodeReference(data);
|
|
418
349
|
} else {
|
|
419
350
|
result = data;
|
|
420
351
|
}
|
|
421
352
|
} finally {
|
|
422
|
-
this
|
|
353
|
+
this.#table = null;
|
|
423
354
|
}
|
|
424
355
|
return result;
|
|
425
356
|
}
|
|
@@ -506,8 +437,8 @@ export class NamespaceResolver {
|
|
|
506
437
|
* object's constructor isn't in the namespace.
|
|
507
438
|
*/
|
|
508
439
|
getConstructor(name: string): new (...args: any[]) => any {
|
|
509
|
-
return (name === "Resurrect.Node" ?
|
|
440
|
+
return (name === "Resurrect.Node" ? FakeNode : this.scope[name] ?? name.split(/\./).reduce((object, name) => {
|
|
510
441
|
return (object as any)[name];
|
|
511
|
-
},
|
|
442
|
+
}, GLOBAL)) as unknown as new () => any;
|
|
512
443
|
}
|
|
513
444
|
}
|