-
Notifications
You must be signed in to change notification settings - Fork 59
Expand file tree
/
Copy patharray.ts
More file actions
287 lines (255 loc) · 8.16 KB
/
array.ts
File metadata and controls
287 lines (255 loc) · 8.16 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
import { $reactive, $reactiveproxy, reactive } from "@reactivedata/reactive";
import * as Y from "yjs";
import { areSame, getYjsValue, INTERNAL_SYMBOL } from ".";
import { Box } from "./boxed";
import { crdtValue, ObjectSchemaType, parseYjsReturnValue } from "./internal";
import { CRDTObject } from "./object";
export type CRDTArray<T> = {
[INTERNAL_SYMBOL]?: Y.Array<T>;
[n: number]: T extends Box<infer A>
? A
: T extends Array<infer A>
? CRDTArray<A>
: T extends ObjectSchemaType
? CRDTObject<T>
: T;
} & T[]; // TODO: should return ArrayImplementation<T> on getter
function arrayImplementation<T>(arr: Y.Array<T>) {
const slice = function slice() {
let ic = this[$reactiveproxy]?.implicitObserver;
(arr as any)._implicitObserver = ic;
const items = arr.slice.bind(arr).apply(arr, arguments);
return items.map((item) => {
const ret = parseYjsReturnValue(item, ic);
if (ic && typeof ret === "object") {
// when using Reactive, we should make sure the returned
// object is made reactive with the implicit observer ic
return reactive(ret, ic);
} else {
return ret;
}
});
} as T[]["slice"];
const wrapItems = function wrapItems(items) {
return items.map((item) => {
const wrapped = crdtValue(item as any); // TODO
let valueToSet = getYjsValue(wrapped) || wrapped;
if (valueToSet instanceof Box) {
valueToSet = valueToSet.value;
}
if (valueToSet instanceof Y.AbstractType && valueToSet.parent) {
throw new Error("Not supported: reassigning object that already occurs in the tree.");
}
return valueToSet;
});
};
const findIndex = function findIndex() {
return [].findIndex.apply(slice.apply(this), arguments);
} as T[]["find"];
const methods = {
// get length() {
// return arr.length;
// },
// set length(val: number) {
// throw new Error("set length of yjs array is unsupported");
// },
slice,
unshift: (...items: T[]) => {
arr.unshift(wrapItems(items));
return (arr as any).lengthUntracked;
},
push: (...items: T[]) => {
arr.push(wrapItems(items));
return (arr as any).lengthUntracked;
},
insert: arr.insert.bind(arr) as Y.Array<T>["insert"],
toJSON: arr.toJSON.bind(arr) as Y.Array<T>["toJSON"],
forEach: function () {
return [].forEach.apply(slice.apply(this), arguments);
} as T[]["forEach"],
every: function () {
return [].every.apply(slice.apply(this), arguments);
},
filter: function () {
return [].filter.apply(slice.apply(this), arguments);
} as T[]["filter"],
find: function () {
return [].find.apply(slice.apply(this), arguments);
} as T[]["find"],
findIndex,
some: function () {
return [].some.apply(slice.apply(this), arguments);
} as T[]["some"],
includes: function () {
return [].includes.apply(slice.apply(this), arguments);
} as T[]["includes"],
map: function () {
return [].map.apply(slice.apply(this), arguments);
} as T[]["map"],
indexOf: function () {
const arg = arguments[0];
return findIndex.call(this, (el) => areSame(el, arg));
} as T[]["indexOf"],
splice: function () {
let start = arguments[0] < 0 ? arr.length - Math.abs(arguments[0]) : arguments[0];
let deleteCount = arguments[1];
let items = Array.from(Array.from(arguments).slice(2));
let deleted = slice.apply(this, [start, Number.isInteger(deleteCount) ? start + deleteCount : undefined]);
if (arr.doc) {
arr.doc.transact(() => {
arr.delete(start, deleteCount);
arr.insert(start, wrapItems(items));
});
} else {
arr.delete(start, deleteCount);
arr.insert(start, wrapItems(items));
}
return deleted;
} as T[]["splice"],
// toJSON = () => {
// return this.arr.toJSON() slice();
// };
// delete = this.arr.delete.bind(this.arr) as (Y.Array<T>)["delete"];
};
const ret = [];
for (let method in methods) {
ret[method] = methods[method];
}
// this is necessary to prevent errors like "trap reported non-configurability for property 'length' which is either non-existent or configurable in the proxy target" when adding support for ownKeys and Reflect.keysx
// (not necessary anymore now we changed ret from object to array)
// Object.defineProperty(ret, "length", {
// enumerable: false,
// configurable: false,
// writable: true,
// value: (arr as any).lengthUntracked,
// });
return ret;
}
function propertyToNumber(p: string | number | symbol) {
if (typeof p === "string" && p.trim().length) {
const asNum = Number(p);
// https://stackoverflow.com/questions/10834796/validate-that-a-string-is-a-positive-integer
if (Number.isInteger(asNum)) {
return asNum;
}
}
return p;
}
export function crdtArray<T>(initializer: T[], arr = new Y.Array<T>()) {
if (arr[$reactive]) {
throw new Error("unexpected");
// arr = arr[$reactive].raw;
}
const implementation = arrayImplementation(arr);
const proxy = new Proxy(implementation as any as CRDTArray<T>, {
set: (target, pArg, value) => {
const p = propertyToNumber(pArg);
if (typeof p !== "number") {
throw new Error();
}
if (arr.doc) {
arr.doc.transact(() => {
arr.delete(p, 1);
arr.insert(p, [value]);
});
} else {
arr.delete(p, 1);
arr.insert(p, [value]);
}
return true;
// // TODO map.set(p, smartValue(value));
// throw new Error("array assignment is not implemented / supported");
},
get: (target, pArg, receiver) => {
const p = propertyToNumber(pArg);
if (p === INTERNAL_SYMBOL) {
return arr;
}
if (typeof p === "number") {
let ic: any;
if (receiver && receiver[$reactiveproxy]) {
ic = receiver[$reactiveproxy]?.implicitObserver;
(arr as any)._implicitObserver = ic;
} else {
// console.warn("no receiver getting property", p);
}
let ret = arr.get(p) as any;
ret = parseYjsReturnValue(ret, ic);
return ret;
}
if (p === Symbol.toStringTag) {
return "Array";
}
if (p === Symbol.iterator) {
const values = arr.slice();
return Reflect.get(values, p);
}
if (p === "length") {
return arr.length;
}
// forward to arrayimplementation
const ret = Reflect.get(target, p, receiver);
return ret;
},
// getOwnPropertyDescriptor: (target, pArg) => {
// const p = propertyToNumber(pArg);
// if (typeof p === "number" && p < arr.length && p >= 0) {
// return { configurable: true, enumerable: true, value: arr.get(p) };
// } else {
// return undefined;
// }
// },
deleteProperty: (target, pArg) => {
const p = propertyToNumber(pArg);
if (typeof p !== "number") {
throw new Error();
}
if (p < (arr as any).lengthUntracked && p >= 0) {
arr.delete(p);
return true;
} else {
return false;
}
},
has: (target, pArg) => {
const p = propertyToNumber(pArg);
if (typeof p !== "number") {
// forward to arrayimplementation
return Reflect.has(target, p);
}
if (p < (arr as any).lengthUntracked && p >= 0) {
return true;
} else {
return false;
}
},
getOwnPropertyDescriptor(target, pArg) {
const p = propertyToNumber(pArg);
if (p === "length") {
return {
enumerable: false,
configurable: false,
writable: true,
};
}
if (typeof p === "number" && p >= 0 && p < (arr as any).lengthUntracked) {
return {
enumerable: true,
configurable: true,
writable: true,
};
}
return undefined;
},
ownKeys: (target) => {
const keys: string[] = [];
for (let i = 0; i < arr.length; i++) {
keys.push(i + "");
}
keys.push("length");
return keys;
},
});
implementation.push.apply(proxy, initializer);
return proxy;
}