@colyseus/schema 5.0.9 → 5.0.10
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 +87 -51
- package/build/codegen/cli.cjs +13 -5
- package/build/codegen/cli.cjs.map +1 -1
- package/build/encoder/ChangeTree.d.ts +10 -2
- package/build/index.cjs +163 -47
- package/build/index.cjs.map +1 -1
- package/build/index.js +163 -47
- package/build/index.mjs +163 -47
- package/build/index.mjs.map +1 -1
- package/build/types/custom/ArraySchema.d.ts +9 -1
- package/build/types/custom/MapSchema.d.ts +18 -1
- package/package.json +9 -3
- package/src/codegen/cli.ts +4 -4
- package/src/codegen/parser.ts +10 -1
- package/src/decoder/DecodeOperation.ts +23 -7
- package/src/encoder/ChangeTree.ts +18 -10
- package/src/encoder/EncodeOperation.ts +14 -1
- package/src/types/custom/ArraySchema.ts +77 -30
- package/src/types/custom/MapSchema.ts +34 -1
|
@@ -55,7 +55,15 @@ export declare class ArraySchema<V = any> implements Array<V>, Collection<number
|
|
|
55
55
|
*/
|
|
56
56
|
pop(): V | undefined;
|
|
57
57
|
at(index: number): V;
|
|
58
|
-
|
|
58
|
+
/**
|
|
59
|
+
* items-index → wire (tmpItems) index. Identity while no deletions are
|
|
60
|
+
* staged this tick; otherwise maps to the index-th live (non-deleted)
|
|
61
|
+
* tmpItems slot — the same live-index walk `splice()` uses. Without the
|
|
62
|
+
* translation, index writes recorded after a same-tick `shift()`/`splice()`
|
|
63
|
+
* land on the wrong wire slots.
|
|
64
|
+
*/
|
|
65
|
+
protected $wireIndex(index: number): number;
|
|
66
|
+
protected $changeAt(index: number, value: V): number | undefined;
|
|
59
67
|
protected $deleteAt(index: number, operation?: OPERATION): void;
|
|
60
68
|
protected $setAt(index: number, value: V, operation: OPERATION): void;
|
|
61
69
|
clear(): void;
|
|
@@ -71,11 +71,28 @@ export declare class MapSchema<V = any, K extends string = string> implements Ma
|
|
|
71
71
|
*/
|
|
72
72
|
static initializeForDecoder<V = any, K extends string = string>(): MapSchema<V, K>;
|
|
73
73
|
/** Iterator */
|
|
74
|
-
[Symbol.iterator]():
|
|
74
|
+
[Symbol.iterator](): ReturnType<Map<K, V>[typeof Symbol.iterator]>;
|
|
75
75
|
get [Symbol.toStringTag](): string;
|
|
76
76
|
static get [Symbol.species](): typeof MapSchema;
|
|
77
77
|
set(key: K, value: V): this;
|
|
78
78
|
get(key: K): V | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Returns the value for `key` if present. Otherwise inserts `defaultValue`
|
|
81
|
+
* (tracked as an ADD change, like `set()`) and returns it.
|
|
82
|
+
*
|
|
83
|
+
* Mirrors `Map.prototype.getOrInsert` (TC39 "upsert" proposal, typed in
|
|
84
|
+
* TypeScript 6's standard library).
|
|
85
|
+
*/
|
|
86
|
+
getOrInsert(key: K, defaultValue: V): V;
|
|
87
|
+
/**
|
|
88
|
+
* Returns the value for `key` if present. Otherwise computes a value via
|
|
89
|
+
* `callbackfn(key)`, inserts it (tracked as an ADD change, like `set()`)
|
|
90
|
+
* and returns it. The callback is only invoked when the key is missing.
|
|
91
|
+
*
|
|
92
|
+
* Mirrors `Map.prototype.getOrInsertComputed` (TC39 "upsert" proposal,
|
|
93
|
+
* typed in TypeScript 6's standard library).
|
|
94
|
+
*/
|
|
95
|
+
getOrInsertComputed(key: K, callbackfn: (key: K) => V): V;
|
|
79
96
|
delete(key: K): boolean;
|
|
80
97
|
clear(): void;
|
|
81
98
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colyseus/schema",
|
|
3
|
-
"version": "5.0.
|
|
3
|
+
"version": "5.0.10",
|
|
4
4
|
"description": "Binary state serializer with delta encoding for games",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"build": "tsc -p tsconfig.build.json && rollup -c rollup.config.mjs",
|
|
18
18
|
"watch": "tsc -p tsconfig.build.json -w",
|
|
19
19
|
"typecheck:build": "tsc -p tsconfig.build.json --noEmit",
|
|
20
|
-
"test": "tsx --tsconfig tsconfig.test.json ./node_modules/mocha/bin/mocha.js \"test/*.test.ts\" \"test/**/*.test.ts\"",
|
|
20
|
+
"test": "npm run test:types && tsx --tsconfig tsconfig.test.json ./node_modules/mocha/bin/mocha.js \"test/*.test.ts\" \"test/**/*.test.ts\"",
|
|
21
|
+
"test:types": "tsc -p tsconfig.build.json && tsc -p tsconfig.types.json",
|
|
21
22
|
"test:exports": "node test/verify-exports.mjs",
|
|
22
23
|
"typecheck": "tsc -p tsconfig.test.json",
|
|
23
24
|
"coverage": "c8 npm run test",
|
|
@@ -96,7 +97,12 @@
|
|
|
96
97
|
"typescript": "^5.9.3"
|
|
97
98
|
},
|
|
98
99
|
"peerDependencies": {
|
|
99
|
-
"typescript": "
|
|
100
|
+
"typescript": ">=5.0.0"
|
|
101
|
+
},
|
|
102
|
+
"peerDependenciesMeta": {
|
|
103
|
+
"typescript": {
|
|
104
|
+
"optional": true
|
|
105
|
+
}
|
|
100
106
|
},
|
|
101
107
|
"c8": {
|
|
102
108
|
"include": [
|
package/src/codegen/cli.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import argv from "./argv.js";
|
|
2
2
|
import { generate, generators } from "./api.js";
|
|
3
3
|
|
|
4
|
-
function displayHelp() {
|
|
4
|
+
function displayHelp(exitCode: number = 0) {
|
|
5
5
|
console.log(`\nschema-codegen [path/to/Schema.ts]
|
|
6
6
|
|
|
7
7
|
Usage (C#/Unity)
|
|
@@ -22,7 +22,7 @@ ${Object.
|
|
|
22
22
|
Optional:
|
|
23
23
|
--namespace: generate namespace on output code
|
|
24
24
|
--decorator: custom name for @type decorator to scan for`);
|
|
25
|
-
process.exit();
|
|
25
|
+
process.exit(exitCode);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
const args = argv(process.argv.slice(2));
|
|
@@ -39,7 +39,7 @@ for (let target in generators) {
|
|
|
39
39
|
|
|
40
40
|
if (!args.output) {
|
|
41
41
|
console.error("You must provide a valid --output directory.");
|
|
42
|
-
displayHelp();
|
|
42
|
+
displayHelp(1);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
try {
|
|
@@ -55,5 +55,5 @@ try {
|
|
|
55
55
|
} catch (e) {
|
|
56
56
|
console.error(e.message);
|
|
57
57
|
console.error(e.stack);
|
|
58
|
-
displayHelp();
|
|
58
|
+
displayHelp(1);
|
|
59
59
|
}
|
package/src/codegen/parser.ts
CHANGED
|
@@ -419,6 +419,14 @@ export function parseFiles(
|
|
|
419
419
|
decoratorName: string = "type",
|
|
420
420
|
context: Context = new Context()
|
|
421
421
|
) {
|
|
422
|
+
if (typeof ts.createSourceFile !== "function") {
|
|
423
|
+
// typescript@7+ (native) no longer ships the JS compiler API
|
|
424
|
+
throw new Error(
|
|
425
|
+
`schema-codegen requires the TypeScript compiler API, which the installed "typescript@${(ts as any).version}" package does not provide.\n` +
|
|
426
|
+
`TypeScript 7+ no longer ships the JS compiler API — install typescript 5.x or 6.x (e.g. \`npm install --save-dev typescript@6\`) to use schema-codegen.`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
422
430
|
/**
|
|
423
431
|
* Re-set globalContext for each test case
|
|
424
432
|
*/
|
|
@@ -469,7 +477,8 @@ export function parseFiles(
|
|
|
469
477
|
|
|
470
478
|
break;
|
|
471
479
|
} catch (e) {
|
|
472
|
-
//
|
|
480
|
+
// only swallow fs errors (ENOENT/EISDIR) while probing alternatives
|
|
481
|
+
if (!e?.code) { throw e; }
|
|
473
482
|
}
|
|
474
483
|
}
|
|
475
484
|
|
|
@@ -349,7 +349,11 @@ export const decodeKeyValueOperation: DecodeOperation = function (
|
|
|
349
349
|
break;
|
|
350
350
|
|
|
351
351
|
case CollectionKind.Array:
|
|
352
|
-
|
|
352
|
+
// resync snapshot ADDs are positional overwrites, not inserts
|
|
353
|
+
tgt.$setAt(index, value,
|
|
354
|
+
(decoder.resyncVisited !== null && operation === OPERATION.ADD)
|
|
355
|
+
? OPERATION.REPLACE
|
|
356
|
+
: operation);
|
|
353
357
|
break;
|
|
354
358
|
|
|
355
359
|
// SetSchema / CollectionSchema / StreamSchema — use the wire-
|
|
@@ -430,9 +434,13 @@ export const decodeArray: DecodeOperation = function (
|
|
|
430
434
|
return;
|
|
431
435
|
|
|
432
436
|
} else if (operation === OPERATION.DELETE_BY_REFID) {
|
|
433
|
-
// TODO: refactor here, try to follow same flow as below
|
|
434
437
|
const refId = decode.number(bytes, it);
|
|
435
438
|
const previousValue = decoder.root.refs.get(refId);
|
|
439
|
+
|
|
440
|
+
// Stale DELETE — refId unknown to this decoder (e.g. it
|
|
441
|
+
// bootstrapped via encodeAll after the item was already removed).
|
|
442
|
+
if (previousValue === undefined) { return; }
|
|
443
|
+
|
|
436
444
|
// Decrement the removed child's ref-count so it can be garbage
|
|
437
445
|
// collected — this refId-based branch returns early and never reaches
|
|
438
446
|
// decodeValue(), so it must do the same DELETE bookkeeping itself.
|
|
@@ -440,10 +448,14 @@ export const decodeArray: DecodeOperation = function (
|
|
|
440
448
|
// different type → "field not defined" / "definition mismatch"
|
|
441
449
|
// (surfaces under StateView when a filtered ArraySchema element is
|
|
442
450
|
// spliced while its parent moves through view membership churn).
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
451
|
+
// Must run even when the item is absent from THIS array (view churn).
|
|
452
|
+
decoder.root.removeRef(refId);
|
|
453
|
+
|
|
446
454
|
index = tgt.findIndex((value: any) => value === previousValue);
|
|
455
|
+
|
|
456
|
+
// Item not present in this decoder's array — nothing to remove locally.
|
|
457
|
+
if (index === -1) { return; }
|
|
458
|
+
|
|
447
459
|
tgt[$deleteByIndex](index);
|
|
448
460
|
allChanges?.push({
|
|
449
461
|
ref,
|
|
@@ -503,9 +515,13 @@ export const decodeArray: DecodeOperation = function (
|
|
|
503
515
|
|
|
504
516
|
if (
|
|
505
517
|
value !== null && value !== undefined &&
|
|
506
|
-
value !== previousValue // avoid setting same value twice (
|
|
518
|
+
value !== previousValue // avoid setting same value twice (an ADD at an occupied index would splice-insert)
|
|
507
519
|
) {
|
|
508
|
-
|
|
520
|
+
// resync snapshot ADDs are positional overwrites, not inserts
|
|
521
|
+
tgt.$setAt(index, value,
|
|
522
|
+
(decoder.resyncVisited !== null && operation === OPERATION.ADD)
|
|
523
|
+
? OPERATION.REPLACE
|
|
524
|
+
: operation);
|
|
509
525
|
}
|
|
510
526
|
|
|
511
527
|
// add change
|
|
@@ -591,12 +591,27 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
591
591
|
this.subscribedViews = undefined;
|
|
592
592
|
}
|
|
593
593
|
|
|
594
|
-
|
|
595
|
-
|
|
594
|
+
/**
|
|
595
|
+
* ArraySchema#unshift(): re-key pending ops on both channels by
|
|
596
|
+
* `+count`, then record ADDs for the new items at indexes 0..count-1.
|
|
597
|
+
*
|
|
598
|
+
* The rebuilt map's insertion order IS the wire order: new ADDs first
|
|
599
|
+
* (ascending — the decoder splice-inserts each one, which only works
|
|
600
|
+
* lowest-index-first), then prior ops in their original relative order
|
|
601
|
+
* at their shifted positions. See ArraySchema#$setAt.
|
|
602
|
+
*/
|
|
603
|
+
unshift(count: number): void {
|
|
604
|
+
if (this._isSchema) throw new Error("ChangeTree (Schema): unshift is not supported");
|
|
596
605
|
const src = this.collDirty!;
|
|
597
606
|
const dst = new Map<number, OPERATION>();
|
|
598
|
-
|
|
607
|
+
const track = !this.paused && !this.isStatic;
|
|
608
|
+
if (track) {
|
|
609
|
+
for (let i = 0; i < count; i++) dst.set(i, OPERATION.ADD);
|
|
610
|
+
}
|
|
611
|
+
for (const [idx, val] of src) dst.set(idx + count, val);
|
|
599
612
|
this.collDirty = dst;
|
|
613
|
+
(this.unreliableRecorder as ICollectionChangeRecorder | undefined)?.shift(count);
|
|
614
|
+
if (track) this.root?.enqueueChangeTree(this);
|
|
600
615
|
}
|
|
601
616
|
|
|
602
617
|
// Tree attachment + child iteration — see ./changeTree/treeAttachment.ts.
|
|
@@ -665,13 +680,6 @@ export class ChangeTree<T extends Ref = any> implements ChangeRecorder {
|
|
|
665
680
|
this._routeAndRecord(index, operation, true);
|
|
666
681
|
}
|
|
667
682
|
|
|
668
|
-
// ArraySchema#unshift(): apply shift to both channels.
|
|
669
|
-
// Unreliable recorder on an array is always a CollectionChangeRecorder.
|
|
670
|
-
shiftChangeIndexes(shiftIndex: number) {
|
|
671
|
-
this.shift(shiftIndex);
|
|
672
|
-
(this.unreliableRecorder as ICollectionChangeRecorder | undefined)?.shift(shiftIndex);
|
|
673
|
-
}
|
|
674
|
-
|
|
675
683
|
getChange(index: number) {
|
|
676
684
|
return this.operationAt(index);
|
|
677
685
|
}
|
|
@@ -224,7 +224,8 @@ export const encodeArray: EncodeOperation = function (
|
|
|
224
224
|
// ArraySchema stores its per-instance child type at `$childType`.
|
|
225
225
|
// This encoder is array-only — there's no Schema fallback to consider.
|
|
226
226
|
const type = ref[$childType];
|
|
227
|
-
const
|
|
227
|
+
const isSchemaChild = typeof type !== "string";
|
|
228
|
+
const useOperationByRefId = hasView && changeTree.isFiltered && isSchemaChild;
|
|
228
229
|
|
|
229
230
|
let refOrIndex: number;
|
|
230
231
|
|
|
@@ -243,6 +244,18 @@ export const encodeArray: EncodeOperation = function (
|
|
|
243
244
|
operation = OPERATION.ADD_BY_REFID;
|
|
244
245
|
}
|
|
245
246
|
|
|
247
|
+
} else if (operation === OPERATION.DELETE && isSchemaChild) {
|
|
248
|
+
//
|
|
249
|
+
// DELETE by identity: idempotent, so a stale positional DELETE
|
|
250
|
+
// (pending in the shared queue when a client bootstraps via
|
|
251
|
+
// encodeAll) can't corrupt that client — its snapshot no longer
|
|
252
|
+
// holds the item, the refId is unknown, and the decoder skips.
|
|
253
|
+
//
|
|
254
|
+
const item = ref.tmpItems[field];
|
|
255
|
+
if (!item) { return; }
|
|
256
|
+
refOrIndex = item[$refId];
|
|
257
|
+
operation = OPERATION.DELETE_BY_REFID;
|
|
258
|
+
|
|
246
259
|
} else {
|
|
247
260
|
refOrIndex = field;
|
|
248
261
|
}
|
|
@@ -44,33 +44,40 @@ const ARRAY_PROXY_HANDLER: ProxyHandler<any> = {
|
|
|
44
44
|
obj.$deleteAt(key as unknown as number);
|
|
45
45
|
|
|
46
46
|
} else {
|
|
47
|
+
// wire slot the write was recorded at; undefined = nothing
|
|
48
|
+
// recorded (same value / skipped) — must NOT touch tmpItems
|
|
49
|
+
// then, or a same-tick shifted layout gets clobbered.
|
|
50
|
+
let wireIndex: number | undefined;
|
|
51
|
+
|
|
47
52
|
if (setValue[$changes]) {
|
|
48
53
|
assertInstanceType(setValue, obj[$childType] as typeof Schema, obj, key);
|
|
49
54
|
|
|
50
55
|
const previousValue = obj.items[key as unknown as number];
|
|
51
56
|
|
|
52
57
|
if (!obj.isMovingItems) {
|
|
53
|
-
obj.$changeAt(Number(key), setValue);
|
|
58
|
+
wireIndex = obj.$changeAt(Number(key), setValue);
|
|
54
59
|
|
|
55
60
|
} else {
|
|
61
|
+
wireIndex = obj.$wireIndex(Number(key));
|
|
62
|
+
|
|
56
63
|
if (previousValue !== undefined) {
|
|
57
64
|
if (setValue[$changes].isNew) {
|
|
58
|
-
obj[$changes].indexedOperation(
|
|
65
|
+
obj[$changes].indexedOperation(wireIndex, OPERATION.MOVE_AND_ADD);
|
|
59
66
|
|
|
60
67
|
} else {
|
|
61
|
-
if ((obj[$changes].getChange(
|
|
62
|
-
obj[$changes].indexedOperation(
|
|
68
|
+
if ((obj[$changes].getChange(wireIndex) & OPERATION.DELETE) === OPERATION.DELETE) {
|
|
69
|
+
obj[$changes].indexedOperation(wireIndex, OPERATION.DELETE_AND_MOVE);
|
|
63
70
|
|
|
64
71
|
} else {
|
|
65
|
-
obj[$changes].indexedOperation(
|
|
72
|
+
obj[$changes].indexedOperation(wireIndex, OPERATION.MOVE);
|
|
66
73
|
}
|
|
67
74
|
}
|
|
68
75
|
|
|
69
76
|
} else if (setValue[$changes].isNew) {
|
|
70
|
-
obj[$changes].indexedOperation(
|
|
77
|
+
obj[$changes].indexedOperation(wireIndex, OPERATION.ADD);
|
|
71
78
|
}
|
|
72
79
|
|
|
73
|
-
setValue[$changes].setParent(obj, obj[$changes].root,
|
|
80
|
+
setValue[$changes].setParent(obj, obj[$changes].root, wireIndex);
|
|
74
81
|
}
|
|
75
82
|
|
|
76
83
|
if (previousValue !== undefined) {
|
|
@@ -79,11 +86,13 @@ const ARRAY_PROXY_HANDLER: ProxyHandler<any> = {
|
|
|
79
86
|
}
|
|
80
87
|
|
|
81
88
|
} else {
|
|
82
|
-
obj.$changeAt(Number(key), setValue);
|
|
89
|
+
wireIndex = obj.$changeAt(Number(key), setValue);
|
|
83
90
|
}
|
|
84
91
|
|
|
85
92
|
obj.items[key as unknown as number] = setValue;
|
|
86
|
-
|
|
93
|
+
if (wireIndex !== undefined) {
|
|
94
|
+
obj.tmpItems[wireIndex] = setValue;
|
|
95
|
+
}
|
|
87
96
|
}
|
|
88
97
|
|
|
89
98
|
return true;
|
|
@@ -300,16 +309,39 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
|
|
|
300
309
|
return this.items[index];
|
|
301
310
|
}
|
|
302
311
|
|
|
303
|
-
|
|
304
|
-
|
|
312
|
+
/**
|
|
313
|
+
* items-index → wire (tmpItems) index. Identity while no deletions are
|
|
314
|
+
* staged this tick; otherwise maps to the index-th live (non-deleted)
|
|
315
|
+
* tmpItems slot — the same live-index walk `splice()` uses. Without the
|
|
316
|
+
* translation, index writes recorded after a same-tick `shift()`/`splice()`
|
|
317
|
+
* land on the wrong wire slots.
|
|
318
|
+
*/
|
|
319
|
+
protected $wireIndex(index: number): number {
|
|
320
|
+
const deletedIndexes = this.deletedIndexes;
|
|
321
|
+
if (deletedIndexes.length === 0) { return index; }
|
|
322
|
+
const tmpItems = this.tmpItems;
|
|
323
|
+
let live = 0;
|
|
324
|
+
for (let i = 0; i < tmpItems.length; i++) {
|
|
325
|
+
if (deletedIndexes[i] !== true) {
|
|
326
|
+
if (live === index) { return i; }
|
|
327
|
+
live++;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// beyond the live range: appends land after the staged tmpItems tail
|
|
331
|
+
return tmpItems.length + (index - live);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// encoding only. Returns the wire index the change was recorded at
|
|
335
|
+
// (undefined when nothing was recorded).
|
|
336
|
+
protected $changeAt(index: number, value: V): number | undefined {
|
|
305
337
|
if (value === undefined || value === null) {
|
|
306
|
-
console.error("ArraySchema items cannot be null nor undefined; Use `
|
|
307
|
-
return;
|
|
338
|
+
console.error("ArraySchema items cannot be null nor undefined; Use `splice(index, 1)` instead.");
|
|
339
|
+
return undefined;
|
|
308
340
|
}
|
|
309
341
|
|
|
310
342
|
// skip if the value is the same as cached.
|
|
311
343
|
if (this.items[index] === value) {
|
|
312
|
-
return;
|
|
344
|
+
return undefined;
|
|
313
345
|
}
|
|
314
346
|
|
|
315
347
|
const operation = (this.items[index] !== undefined)
|
|
@@ -318,30 +350,34 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
|
|
|
318
350
|
: OPERATION.REPLACE // primitive
|
|
319
351
|
: OPERATION.ADD;
|
|
320
352
|
|
|
353
|
+
const wireIndex = this.$wireIndex(index);
|
|
354
|
+
|
|
321
355
|
const changeTree = this[$changes];
|
|
322
|
-
changeTree.change(
|
|
356
|
+
changeTree.change(wireIndex, operation);
|
|
323
357
|
|
|
324
358
|
//
|
|
325
359
|
// set value's parent after the value is set
|
|
326
360
|
// (to avoid encoding "refId" operations before parent's "ADD" operation)
|
|
327
361
|
//
|
|
328
|
-
value[$changes]?.setParent(this, changeTree.root,
|
|
362
|
+
value[$changes]?.setParent(this, changeTree.root, wireIndex);
|
|
363
|
+
|
|
364
|
+
return wireIndex;
|
|
329
365
|
}
|
|
330
366
|
|
|
331
367
|
// encoding only
|
|
332
368
|
protected $deleteAt(index: number, operation?: OPERATION) {
|
|
333
|
-
this[$changes].delete(index, operation);
|
|
369
|
+
this[$changes].delete(this.$wireIndex(index), operation);
|
|
334
370
|
}
|
|
335
371
|
|
|
336
372
|
// decoding only
|
|
337
373
|
protected $setAt(index: number, value: V, operation: OPERATION) {
|
|
338
374
|
if (
|
|
339
|
-
index === 0 &&
|
|
340
375
|
operation === OPERATION.ADD &&
|
|
341
376
|
this.items[index] !== undefined
|
|
342
377
|
) {
|
|
343
|
-
//
|
|
344
|
-
|
|
378
|
+
// ADD at an occupied index = insert (unshift / splice-insert):
|
|
379
|
+
// shift existing items up instead of overwriting.
|
|
380
|
+
this.items.splice(index, 0, value);
|
|
345
381
|
|
|
346
382
|
} else if (operation === OPERATION.DELETE_AND_MOVE) {
|
|
347
383
|
this.items.splice(index, 1);
|
|
@@ -436,10 +472,14 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
|
|
|
436
472
|
if (items.length === 0) { return undefined; }
|
|
437
473
|
|
|
438
474
|
const changeTree = self[$changes];
|
|
439
|
-
|
|
440
|
-
|
|
475
|
+
// items[0] ≡ first live (non-deleted) tmpItems slot. Value-based
|
|
476
|
+
// findIndex is unsafe here: same-tick index writes can duplicate a
|
|
477
|
+
// value across tmp slots and resolve the wrong one.
|
|
478
|
+
const deletedIndexes = self.deletedIndexes;
|
|
479
|
+
let index = 0;
|
|
480
|
+
while (deletedIndexes[index] === true) { index++; }
|
|
441
481
|
changeTree.delete(index, OPERATION.DELETE);
|
|
442
|
-
|
|
482
|
+
deletedIndexes[index] = true;
|
|
443
483
|
|
|
444
484
|
return items.shift();
|
|
445
485
|
}
|
|
@@ -564,13 +604,20 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V>, IR
|
|
|
564
604
|
const self = this[$proxyTarget];
|
|
565
605
|
const changeTree = self[$changes];
|
|
566
606
|
|
|
567
|
-
//
|
|
568
|
-
//
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
items
|
|
572
|
-
|
|
573
|
-
|
|
607
|
+
// single recorder op: shifts pending indexes up and records the new
|
|
608
|
+
// ADDs lowest-first (the decoder splice-inserts in ascending order).
|
|
609
|
+
changeTree.unshift(items.length);
|
|
610
|
+
|
|
611
|
+
// attach ref-type items — parent set AFTER recording, as in $changeAt
|
|
612
|
+
for (let i = 0; i < items.length; i++) {
|
|
613
|
+
items[i]?.[$changes]?.setParent(this, changeTree.root, i);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// keep staged-delete flags aligned with the prepended tmp slots
|
|
617
|
+
const deletedIndexes = self.deletedIndexes;
|
|
618
|
+
if (deletedIndexes.length > 0) {
|
|
619
|
+
deletedIndexes.unshift(...new Array(items.length).fill(false));
|
|
620
|
+
}
|
|
574
621
|
|
|
575
622
|
self.tmpItems.unshift(...items);
|
|
576
623
|
|
|
@@ -140,7 +140,7 @@ export class MapSchema<V=any, K extends string = string> implements Map<K, V>, C
|
|
|
140
140
|
}
|
|
141
141
|
|
|
142
142
|
/** Iterator */
|
|
143
|
-
[Symbol.iterator]():
|
|
143
|
+
[Symbol.iterator](): ReturnType<Map<K, V>[typeof Symbol.iterator]> { return this.$items[Symbol.iterator](); }
|
|
144
144
|
get [Symbol.toStringTag]() { return this.$items[Symbol.toStringTag] }
|
|
145
145
|
|
|
146
146
|
static get [Symbol.species]() { return MapSchema; }
|
|
@@ -224,6 +224,39 @@ export class MapSchema<V=any, K extends string = string> implements Map<K, V>, C
|
|
|
224
224
|
return this.$items.get(key);
|
|
225
225
|
}
|
|
226
226
|
|
|
227
|
+
/**
|
|
228
|
+
* Returns the value for `key` if present. Otherwise inserts `defaultValue`
|
|
229
|
+
* (tracked as an ADD change, like `set()`) and returns it.
|
|
230
|
+
*
|
|
231
|
+
* Mirrors `Map.prototype.getOrInsert` (TC39 "upsert" proposal, typed in
|
|
232
|
+
* TypeScript 6's standard library).
|
|
233
|
+
*/
|
|
234
|
+
getOrInsert(key: K, defaultValue: V): V {
|
|
235
|
+
if (this.$items.has(key)) {
|
|
236
|
+
return this.$items.get(key);
|
|
237
|
+
}
|
|
238
|
+
this.set(key, defaultValue);
|
|
239
|
+
return defaultValue;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Returns the value for `key` if present. Otherwise computes a value via
|
|
244
|
+
* `callbackfn(key)`, inserts it (tracked as an ADD change, like `set()`)
|
|
245
|
+
* and returns it. The callback is only invoked when the key is missing.
|
|
246
|
+
*
|
|
247
|
+
* Mirrors `Map.prototype.getOrInsertComputed` (TC39 "upsert" proposal,
|
|
248
|
+
* typed in TypeScript 6's standard library).
|
|
249
|
+
*/
|
|
250
|
+
getOrInsertComputed(key: K, callbackfn: (key: K) => V): V {
|
|
251
|
+
if (this.$items.has(key)) {
|
|
252
|
+
return this.$items.get(key);
|
|
253
|
+
}
|
|
254
|
+
const value = callbackfn(key);
|
|
255
|
+
// per spec: overwrites even if callbackfn itself inserted `key`
|
|
256
|
+
this.set(key, value);
|
|
257
|
+
return value;
|
|
258
|
+
}
|
|
259
|
+
|
|
227
260
|
delete(key: K) {
|
|
228
261
|
if (!this.$items.has(key)) {
|
|
229
262
|
return false;
|