Skip to content

Commit 10e59e7

Browse files
committed
fix: flow
1 parent eccdf4f commit 10e59e7

File tree

9 files changed

+191
-54
lines changed

9 files changed

+191
-54
lines changed

README.md

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@
44

55
A high-performance low-level state management system for games :video_game:. Contains supercharged [proxies](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy).
66

7-
Propertea supports:
7+
## :fire: Features
88

9-
- Change/dirty tracking ([`MarkClean`](#), [`onDirty`](#))
10-
- Creating and applying diffs ([`Diff`](#), [`Set`](#), [`SetWithDefaults`](#))
11-
- Full (de)serialization to/from JSON ([`ToJSON`](#), [`ToJSONWithoutDefaults`](#))
12-
- Fixed-size binary representation for efficient state transformations using `TypedArray` or WASM.
9+
- Change/dirty tracking
10+
- Creating and applying diffs
11+
- Full (de)serialization to/from JSON
12+
- Fixed-size contiguous binary representation for efficient state transformations using `TypedArray` or even WASM.
1313
- Object pooling
1414

15-
## Examples
15+
## Examples :mag:
1616

1717
### Pool (fixed-size)
1818

@@ -79,7 +79,7 @@ const user = userPool.allocate();
7979
assert(user.age === 0);
8080
assert(user.name === '');
8181
// same structure, null buffer
82-
assert(userPool.data.memory.buffer.byteLength === 0)
82+
assert(userPool.data.memory.buffer.byteLength === 0);
8383
```
8484

8585
Notice that even though `age` is a `uint32` (a fixed-size type), **the type becomes dynamic-sized if any type contained within is dynamic-sized**.
@@ -111,27 +111,35 @@ assert(another[Diff]() === undefined);
111111
Changes may trigger a callback:
112112

113113
```js
114-
const reactivePool = new Pool({
114+
const blueprint = {
115115
type: 'object',
116116
properties: {
117117
foo: {type: 'string'},
118118
},
119-
}, {
119+
};
120+
const params = {
120121
onDirty: (
121122
// the dirty bit
122123
bit,
123124
// the proxy triggering this change
124125
proxy,
125126
) => {
126127
// ... do something!
127-
console.log('dirty!');
128+
console.log('bit:', bit, 'proxy:', proxy);
128129
}
129-
});
130+
};
131+
const reactivePool = new Pool(blueprint, params);
132+
reactivePool.allocate();
133+
// bit: 0 proxy: ConcreteProxy {
134+
// [Symbol(element)]: { foo: '' },
135+
// [Symbol(DataOffset)]: 0,
136+
// [Symbol(DirtyOffset)]: 0
137+
// }
130138
```
131139

132140
### WASM
133141

134-
Pools are structured to be operated on by WASM. See [src/pool.test.wat](./src/pool.test.wat) for a minimal example of using WASM to transform data.
142+
Pools are structured to be operated on by WASM. See [src/pool.test.wat](./src/pool.test.wat) for a minimal example of using WASM to transform data (and track changes).
135143

136144
Excerpted from [src/pool.test.js](./src/pool.test.js):
137145

@@ -179,15 +187,18 @@ Networked real-time applications with arbitrarily-large mutable state (read: gam
179187

180188
### Performance
181189

182-
It is greatly beneficial for performance when data is arranged contiguously so that e.g. SIMD may be leveraged for data transformations.
190+
Code generation (`new Function`) is used to generate a [monomorphic](https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html) proxy shape from a blueprint. This keeps the [inline cache](https://mathiasbynens.be/notes/shapes-ics) hot and performant.
183191

184-
This library is *fast*. As you can see in [`src/pool.bench.js`](./src/pool.bench.js), Propertea beats native JavaScript by 100-1000x transforming contiguous data.
192+
It is greatly beneficial for performance when data is arranged contiguously so that e.g. SIMD may be leveraged for data transformations.
185193

186-
Pooled allocations actually beat native after warming the pool.
194+
This library is *fast*. As you can see in [`src/pool.bench.js`](./src/pool.bench.js), Propertea beats native JavaScript by 100-1000x transforming contiguous data. Pooled allocations actually beat native after warming the pool.
187195

188-
Code generation (`new Function`) is used to generate a [monomorphic](https://mrale.ph/blog/2015/01/11/whats-up-with-monomorphism.html) proxy shape, based off the blueprint.
196+
### Onward and upward
189197

190-
### etc...
198+
Specifically, this is motivated by my pure JavaScript ECS which is the next to be released.
191199

192-
Specifically, this is motivated by my pure JavaScript ECS that is the next to be open sourced.
200+
## TODO
193201

202+
- Fixed-length arrays
203+
- Fixed-shape maps (depends on `crunches` codec support)
204+
- More array proxy ops

src/array.js

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import {registry} from './register.js';
77
const Key = Symbol('Index');
88
const ArraySymbol = Symbol('ArraySymbol');
99

10+
const nop = () => {};
11+
1012
registry.array = class extends ProxyProperty {
1113

1214
constructor(blueprint) {
@@ -29,6 +31,8 @@ registry.array = class extends ProxyProperty {
2931
generateProxy(views) {
3032
const {blueprint, property} = this;
3133
const {dirtyWidth} = property;
34+
const onDirty = views.onDirty ?? true;
35+
const onDirtyCallback = 'function' === typeof onDirty ? onDirty : nop;
3236
let Concrete;
3337
let pool;
3438
if (property instanceof ProxyProperty) {
@@ -41,15 +45,15 @@ registry.array = class extends ProxyProperty {
4145
[ArraySymbol] = undefined;
4246
},
4347
},
44-
{
48+
onDirty ? {
4549
onDirty: (bit, proxy) => {
46-
views.onDirty?.(bit, proxy);
50+
onDirtyCallback(bit, proxy);
4751
const index = Math.floor(bit / dirtyWidth);
4852
if (index < pool.length.value) {
4953
pool.proxies[index][ArraySymbol].dirty.add(pool.proxies[index][Key]);
5054
}
5155
},
52-
},
56+
} : {},
5357
);
5458
}
5559

@@ -58,6 +62,7 @@ registry.array = class extends ProxyProperty {
5862

5963
constructor() {
6064
super();
65+
this[ProperteaSet](blueprint.defaultValue);
6166
}
6267

6368
dirty = new Set();
@@ -67,13 +72,25 @@ registry.array = class extends ProxyProperty {
6772
if (property instanceof ProxyProperty) {
6873
pool.free(this[i]);
6974
}
70-
this.dirty.add(i);
75+
if (onDirty) {
76+
this.dirty.add(i);
77+
}
7178
}
7279
super.length = length;
7380
}
7481

7582
setAt(key, value) {
76-
if (property instanceof ProxyProperty) {
83+
if (undefined === value) {
84+
if (key in this) {
85+
pool.free(this[key]);
86+
}
87+
if (onDirty) {
88+
this.dirty.add(key);
89+
}
90+
}
91+
const isProxy = property instanceof ProxyProperty;
92+
let previous;
93+
if (isProxy) {
7794
if (this[key]) {
7895
this[key][ProperteaSet](value instanceof Concrete ? value[ToJSON]() : value);
7996
value = this[key];
@@ -87,8 +104,18 @@ registry.array = class extends ProxyProperty {
87104
value = localValue;
88105
}
89106
}
90-
this.dirty.add(key);
107+
else {
108+
previous = this[key];
109+
}
110+
if (onDirty) {
111+
this.dirty.add(key);
112+
}
91113
this[key] = value;
114+
if (!isProxy) {
115+
if (previous !== value) {
116+
onDirtyCallback(parseInt(key), this);
117+
}
118+
}
92119
}
93120

94121
[ToJSON]() {
@@ -125,12 +152,24 @@ registry.array = class extends ProxyProperty {
125152
}
126153
};
127154
}
128-
ArrayProxy.prototype[SetWithDefaults] = function() {};
129-
ArrayProxy.prototype[ProperteaSet] = function(iterable) {
130-
this.setLength(0);
131-
let i = 0;
132-
for (const value of iterable) {
133-
this.setAt(i++, value);
155+
ArrayProxy.prototype[SetWithDefaults] = function(value) {
156+
this[ProperteaSet](value);
157+
};
158+
ArrayProxy.prototype[ProperteaSet] = function(iterableOrDiff) {
159+
if (!iterableOrDiff || 'object' !== typeof iterableOrDiff) {
160+
return;
161+
}
162+
if (Symbol.iterator in iterableOrDiff) {
163+
this.setLength(0);
164+
let i = 0;
165+
for (const value of iterableOrDiff) {
166+
this.setAt(i++, value);
167+
}
168+
}
169+
else {
170+
for (const key in iterableOrDiff) {
171+
this.setAt(parseInt(key), iterableOrDiff[key]);
172+
}
134173
}
135174
};
136175
return ArrayProxy;

src/array.test.js

Lines changed: 67 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,22 @@ import './primitives.js';
66
import {Diff, MarkClean, Set, ToJSON} from './proxy.js';
77
import {registry} from './register.js';
88

9+
test('default value', () => {
10+
const property = new registry.array({
11+
defaultValue: [1, 2, 3],
12+
element: {type: 'uint8'},
13+
});
14+
const Proxy = property.concrete();
15+
const proxy = new Proxy();
16+
expect(proxy[Diff]()).toEqual({0: 1, 1: 2, 2: 3});
17+
});
18+
919
test('primitive', () => {
1020
const property = new registry.array({
1121
element: {type: 'uint8'},
1222
});
13-
const Map = property.concrete();
14-
const proxy = new Map();
23+
const Proxy = property.concrete();
24+
const proxy = new Proxy();
1525
proxy.setAt(0, 1);
1626
expect(proxy[0]).toEqual(1);
1727
expect(proxy[Diff]()).toEqual({0: 1});
@@ -21,8 +31,8 @@ test('proxy', () => {
2131
const property = new registry.array({
2232
element: {type: 'object', properties: {x: {type: 'uint8'}}},
2333
});
24-
const Map = property.concrete();
25-
const proxy = new Map();
34+
const Proxy = property.concrete();
35+
const proxy = new Proxy();
2636
const value = {x: 4};
2737
proxy.setAt(0, value);
2838
const first = proxy[0];
@@ -50,8 +60,8 @@ test('within', () => {
5060
x: {type: 'array', element: {type: 'uint8'}},
5161
},
5262
});
53-
const Map = property.concrete();
54-
const proxy = new Map();
63+
const Proxy = property.concrete();
64+
const proxy = new Proxy();
5565
const value = [1, 2, 3];
5666
proxy.x = value;
5767
expect(proxy.x[ToJSON]()).not.toBe(value);
@@ -60,3 +70,54 @@ test('within', () => {
6070
proxy.x.setAt(1, 3);
6171
expect(proxy[Diff]()).toEqual({x: {1: 3}});
6272
});
73+
74+
test('set partial', () => {
75+
const property = new registry.array({
76+
element: {type: 'uint8'},
77+
});
78+
const Proxy = property.concrete();
79+
const proxy = new Proxy();
80+
proxy.setAt(0, 1);
81+
proxy.setAt(1, 2);
82+
proxy[Set]({2: 3});
83+
expect(proxy[Diff]()).toEqual({0: 1, 1: 2, 2: 3});
84+
});
85+
86+
test('reactivity', () => {
87+
let dirties = 0;
88+
const property = new registry.array({
89+
element: {type: 'uint8'},
90+
});
91+
const Proxy = property.concrete({
92+
onDirty: () => { dirties += 1; }
93+
});
94+
const proxy = new Proxy();
95+
expect(dirties).toEqual(0);
96+
proxy.setAt(0, 1);
97+
expect(dirties).toEqual(1);
98+
proxy.setAt(1, 2);
99+
expect(dirties).toEqual(2);
100+
proxy[Set]({2: 3});
101+
expect(proxy[Diff]()).toEqual({0: 1, 1: 2, 2: 3});
102+
});
103+
104+
test('reactivity (proxy)', () => {
105+
let dirties = 0;
106+
const property = new registry.array({
107+
element: {
108+
type: 'object',
109+
properties: {
110+
x: {type: 'uint8'},
111+
},
112+
},
113+
});
114+
const Proxy = property.concrete({
115+
onDirty: () => { dirties += 1; }
116+
});
117+
const proxy = new Proxy();
118+
expect(dirties).toEqual(0);
119+
proxy.setAt(0, {x: 3});
120+
expect(dirties).toEqual(2);
121+
proxy[0].x = 4;
122+
expect(dirties).toEqual(3);
123+
});

src/index.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ import './map.js';
55
import './object.js';
66
export {Index, Pool} from './pool.js';
77
export {Property} from './property.js';
8-
export {Diff, MarkClean, Set, SetWithDefaults, ToJSON, ToJSONWithoutDefaults} from './proxy.js';
8+
export {Diff, Instance, MarkClean, Set, SetWithDefaults, ToJSON, ToJSONWithoutDefaults} from './proxy.js';
99
export {registry} from './register.js';

src/map.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@ registry.map = class extends ProxyProperty {
1414
if (!registry[blueprint.value.type]) {
1515
throw new TypeError(`Propertea(map): value type '${blueprint.value.type}' not registered`);
1616
}
17-
const property = new registry[blueprint.value.type](blueprint.value);
18-
this.property = property;
17+
this.property = new registry[blueprint.value.type](blueprint.value);
1918
this.codec = new Codecs.map(blueprint);
2019
}
2120

@@ -59,6 +58,7 @@ registry.map = class extends ProxyProperty {
5958

6059
constructor() {
6160
super();
61+
this[ProperteaSet](blueprint.defaultValue);
6262
}
6363

6464
clear() {
@@ -130,6 +130,9 @@ registry.map = class extends ProxyProperty {
130130
}
131131
MapProxy.prototype[SetWithDefaults] = function() {};
132132
MapProxy.prototype[ProperteaSet] = function(entries) {
133+
if (!entries) {
134+
return;
135+
}
133136
this.clear();
134137
for (const entry of entries) {
135138
this.set(entry[0], entry[1]);

0 commit comments

Comments
 (0)