@sylphx/lens-solid 2.3.3 → 2.3.5
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/dist/index.js +1241 -321
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -464,6 +464,229 @@ class EntityBuilder_ {
|
|
|
464
464
|
return createEntityDef(this._name, fields);
|
|
465
465
|
}
|
|
466
466
|
}
|
|
467
|
+
var valueStrategy = {
|
|
468
|
+
name: "value",
|
|
469
|
+
encode(_prev, next) {
|
|
470
|
+
return { strategy: "value", data: next };
|
|
471
|
+
},
|
|
472
|
+
decode(_current, update) {
|
|
473
|
+
return update.data;
|
|
474
|
+
},
|
|
475
|
+
estimateSize(update) {
|
|
476
|
+
return JSON.stringify(update.data).length;
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
var deltaStrategy = {
|
|
480
|
+
name: "delta",
|
|
481
|
+
encode(prev, next) {
|
|
482
|
+
const operations2 = computeStringDiff(prev, next);
|
|
483
|
+
const diffSize = JSON.stringify(operations2).length;
|
|
484
|
+
const valueSize = next.length + 20;
|
|
485
|
+
if (diffSize >= valueSize) {
|
|
486
|
+
return { strategy: "value", data: next };
|
|
487
|
+
}
|
|
488
|
+
return { strategy: "delta", data: operations2 };
|
|
489
|
+
},
|
|
490
|
+
decode(current, update) {
|
|
491
|
+
if (update.strategy === "value") {
|
|
492
|
+
return update.data;
|
|
493
|
+
}
|
|
494
|
+
const operations2 = update.data;
|
|
495
|
+
return applyStringDiff(current, operations2);
|
|
496
|
+
},
|
|
497
|
+
estimateSize(update) {
|
|
498
|
+
return JSON.stringify(update.data).length;
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
function computeStringDiff(prev, next) {
|
|
502
|
+
const operations2 = [];
|
|
503
|
+
let prefixLen = 0;
|
|
504
|
+
const minLen = Math.min(prev.length, next.length);
|
|
505
|
+
while (prefixLen < minLen && prev[prefixLen] === next[prefixLen]) {
|
|
506
|
+
prefixLen++;
|
|
507
|
+
}
|
|
508
|
+
let suffixLen = 0;
|
|
509
|
+
const remainingPrev = prev.length - prefixLen;
|
|
510
|
+
const remainingNext = next.length - prefixLen;
|
|
511
|
+
const maxSuffix = Math.min(remainingPrev, remainingNext);
|
|
512
|
+
while (suffixLen < maxSuffix && prev[prev.length - 1 - suffixLen] === next[next.length - 1 - suffixLen]) {
|
|
513
|
+
suffixLen++;
|
|
514
|
+
}
|
|
515
|
+
const deleteCount = prev.length - prefixLen - suffixLen;
|
|
516
|
+
const insertText = next.slice(prefixLen, next.length - suffixLen || undefined);
|
|
517
|
+
if (deleteCount > 0 || insertText.length > 0) {
|
|
518
|
+
operations2.push({
|
|
519
|
+
position: prefixLen,
|
|
520
|
+
...deleteCount > 0 ? { delete: deleteCount } : {},
|
|
521
|
+
...insertText.length > 0 ? { insert: insertText } : {}
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
return operations2;
|
|
525
|
+
}
|
|
526
|
+
function applyStringDiff(current, operations2) {
|
|
527
|
+
let result = current;
|
|
528
|
+
const sortedOps = [...operations2].sort((a, b) => b.position - a.position);
|
|
529
|
+
for (const op2 of sortedOps) {
|
|
530
|
+
const before = result.slice(0, op2.position);
|
|
531
|
+
const after = result.slice(op2.position + (op2.delete ?? 0));
|
|
532
|
+
result = before + (op2.insert ?? "") + after;
|
|
533
|
+
}
|
|
534
|
+
return result;
|
|
535
|
+
}
|
|
536
|
+
var patchStrategy = {
|
|
537
|
+
name: "patch",
|
|
538
|
+
encode(prev, next) {
|
|
539
|
+
const operations2 = computeJsonPatch(prev, next);
|
|
540
|
+
const patchSize = JSON.stringify(operations2).length;
|
|
541
|
+
const valueSize = JSON.stringify(next).length + 20;
|
|
542
|
+
if (patchSize >= valueSize) {
|
|
543
|
+
return { strategy: "value", data: next };
|
|
544
|
+
}
|
|
545
|
+
return { strategy: "patch", data: operations2 };
|
|
546
|
+
},
|
|
547
|
+
decode(current, update) {
|
|
548
|
+
if (update.strategy === "value") {
|
|
549
|
+
return update.data;
|
|
550
|
+
}
|
|
551
|
+
const operations2 = update.data;
|
|
552
|
+
return applyJsonPatch(current, operations2);
|
|
553
|
+
},
|
|
554
|
+
estimateSize(update) {
|
|
555
|
+
return JSON.stringify(update.data).length;
|
|
556
|
+
}
|
|
557
|
+
};
|
|
558
|
+
function computeJsonPatch(prev, next, basePath = "") {
|
|
559
|
+
const operations2 = [];
|
|
560
|
+
const prevObj = prev;
|
|
561
|
+
const nextObj = next;
|
|
562
|
+
for (const key of Object.keys(prevObj)) {
|
|
563
|
+
if (!(key in nextObj)) {
|
|
564
|
+
operations2.push({ op: "remove", path: `${basePath}/${escapeJsonPointer(key)}` });
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
for (const [key, nextValue] of Object.entries(nextObj)) {
|
|
568
|
+
const path = `${basePath}/${escapeJsonPointer(key)}`;
|
|
569
|
+
const prevValue = prevObj[key];
|
|
570
|
+
if (!(key in prevObj)) {
|
|
571
|
+
operations2.push({ op: "add", path, value: nextValue });
|
|
572
|
+
} else if (!deepEqual2(prevValue, nextValue)) {
|
|
573
|
+
if (isPlainObject(prevValue) && isPlainObject(nextValue) && !Array.isArray(prevValue) && !Array.isArray(nextValue)) {
|
|
574
|
+
operations2.push(...computeJsonPatch(prevValue, nextValue, path));
|
|
575
|
+
} else {
|
|
576
|
+
operations2.push({ op: "replace", path, value: nextValue });
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
return operations2;
|
|
581
|
+
}
|
|
582
|
+
function applyJsonPatch(current, operations2) {
|
|
583
|
+
const result = structuredClone(current);
|
|
584
|
+
for (const op2 of operations2) {
|
|
585
|
+
const pathParts = parseJsonPointer(op2.path);
|
|
586
|
+
switch (op2.op) {
|
|
587
|
+
case "add":
|
|
588
|
+
case "replace":
|
|
589
|
+
setValueAtPath(result, pathParts, op2.value);
|
|
590
|
+
break;
|
|
591
|
+
case "remove":
|
|
592
|
+
removeValueAtPath(result, pathParts);
|
|
593
|
+
break;
|
|
594
|
+
case "move":
|
|
595
|
+
if (op2.from) {
|
|
596
|
+
const fromParts = parseJsonPointer(op2.from);
|
|
597
|
+
const value = getValueAtPath(result, fromParts);
|
|
598
|
+
removeValueAtPath(result, fromParts);
|
|
599
|
+
setValueAtPath(result, pathParts, value);
|
|
600
|
+
}
|
|
601
|
+
break;
|
|
602
|
+
case "copy":
|
|
603
|
+
if (op2.from) {
|
|
604
|
+
const fromParts = parseJsonPointer(op2.from);
|
|
605
|
+
const value = structuredClone(getValueAtPath(result, fromParts));
|
|
606
|
+
setValueAtPath(result, pathParts, value);
|
|
607
|
+
}
|
|
608
|
+
break;
|
|
609
|
+
case "test":
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return result;
|
|
614
|
+
}
|
|
615
|
+
function applyUpdate(current, update) {
|
|
616
|
+
switch (update.strategy) {
|
|
617
|
+
case "value":
|
|
618
|
+
return valueStrategy.decode(current, update);
|
|
619
|
+
case "delta":
|
|
620
|
+
return deltaStrategy.decode(current, update);
|
|
621
|
+
case "patch":
|
|
622
|
+
return patchStrategy.decode(current, update);
|
|
623
|
+
default:
|
|
624
|
+
return update.data;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
function isPlainObject(value) {
|
|
628
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
629
|
+
}
|
|
630
|
+
function deepEqual2(a, b) {
|
|
631
|
+
if (a === b)
|
|
632
|
+
return true;
|
|
633
|
+
if (typeof a !== typeof b)
|
|
634
|
+
return false;
|
|
635
|
+
if (typeof a !== "object" || a === null || b === null)
|
|
636
|
+
return false;
|
|
637
|
+
const aKeys = Object.keys(a);
|
|
638
|
+
const bKeys = Object.keys(b);
|
|
639
|
+
if (aKeys.length !== bKeys.length)
|
|
640
|
+
return false;
|
|
641
|
+
for (const key of aKeys) {
|
|
642
|
+
if (!deepEqual2(a[key], b[key])) {
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return true;
|
|
647
|
+
}
|
|
648
|
+
function escapeJsonPointer(str) {
|
|
649
|
+
return str.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
650
|
+
}
|
|
651
|
+
function parseJsonPointer(path) {
|
|
652
|
+
if (!path || path === "/")
|
|
653
|
+
return [];
|
|
654
|
+
return path.slice(1).split("/").map((p) => p.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
655
|
+
}
|
|
656
|
+
function getValueAtPath(obj, path) {
|
|
657
|
+
let current = obj;
|
|
658
|
+
for (const key of path) {
|
|
659
|
+
if (current === null || typeof current !== "object")
|
|
660
|
+
return;
|
|
661
|
+
current = current[key];
|
|
662
|
+
}
|
|
663
|
+
return current;
|
|
664
|
+
}
|
|
665
|
+
function setValueAtPath(obj, path, value) {
|
|
666
|
+
if (path.length === 0)
|
|
667
|
+
return;
|
|
668
|
+
let current = obj;
|
|
669
|
+
for (let i = 0;i < path.length - 1; i++) {
|
|
670
|
+
const key = path[i];
|
|
671
|
+
if (!(key in current) || typeof current[key] !== "object") {
|
|
672
|
+
current[key] = {};
|
|
673
|
+
}
|
|
674
|
+
current = current[key];
|
|
675
|
+
}
|
|
676
|
+
current[path[path.length - 1]] = value;
|
|
677
|
+
}
|
|
678
|
+
function removeValueAtPath(obj, path) {
|
|
679
|
+
if (path.length === 0)
|
|
680
|
+
return;
|
|
681
|
+
let current = obj;
|
|
682
|
+
for (let i = 0;i < path.length - 1; i++) {
|
|
683
|
+
const key = path[i];
|
|
684
|
+
if (!(key in current) || typeof current[key] !== "object")
|
|
685
|
+
return;
|
|
686
|
+
current = current[key];
|
|
687
|
+
}
|
|
688
|
+
delete current[path[path.length - 1]];
|
|
689
|
+
}
|
|
467
690
|
var OPTIMISTIC_PLUGIN_SYMBOL = Symbol.for("lens:optimistic-plugin");
|
|
468
691
|
var DEFAULT_OPERATION_LOG_CONFIG = {
|
|
469
692
|
maxEntries: 1e4,
|
|
@@ -836,18 +1059,744 @@ class ReconnectionMetricsTracker {
|
|
|
836
1059
|
for (const result of results) {
|
|
837
1060
|
counts[result.status] = (counts[result.status] ?? 0) + 1;
|
|
838
1061
|
}
|
|
839
|
-
return counts;
|
|
840
|
-
}
|
|
841
|
-
percentile(p) {
|
|
842
|
-
if (this.latencies.length === 0)
|
|
843
|
-
return 0;
|
|
844
|
-
const sorted = [...this.latencies].sort((a, b) => a - b);
|
|
845
|
-
const index = Math.ceil(p / 100 * sorted.length) - 1;
|
|
846
|
-
return sorted[Math.max(0, index)];
|
|
1062
|
+
return counts;
|
|
1063
|
+
}
|
|
1064
|
+
percentile(p) {
|
|
1065
|
+
if (this.latencies.length === 0)
|
|
1066
|
+
return 0;
|
|
1067
|
+
const sorted = [...this.latencies].sort((a, b) => a - b);
|
|
1068
|
+
const index = Math.ceil(p / 100 * sorted.length) - 1;
|
|
1069
|
+
return sorted[Math.max(0, index)];
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
class SubscriptionRegistry {
|
|
1073
|
+
subscriptions = new Map;
|
|
1074
|
+
entityIndex = new Map;
|
|
1075
|
+
add(sub) {
|
|
1076
|
+
const tracked = {
|
|
1077
|
+
...sub,
|
|
1078
|
+
state: "pending",
|
|
1079
|
+
lastDataHash: sub.lastData ? hashEntityState(sub.lastData) : null,
|
|
1080
|
+
createdAt: Date.now(),
|
|
1081
|
+
lastUpdateAt: null
|
|
1082
|
+
};
|
|
1083
|
+
this.subscriptions.set(sub.id, tracked);
|
|
1084
|
+
const entityKey = `${sub.entity}:${sub.entityId}`;
|
|
1085
|
+
let ids = this.entityIndex.get(entityKey);
|
|
1086
|
+
if (!ids) {
|
|
1087
|
+
ids = new Set;
|
|
1088
|
+
this.entityIndex.set(entityKey, ids);
|
|
1089
|
+
}
|
|
1090
|
+
ids.add(sub.id);
|
|
1091
|
+
}
|
|
1092
|
+
get(id2) {
|
|
1093
|
+
return this.subscriptions.get(id2);
|
|
1094
|
+
}
|
|
1095
|
+
has(id2) {
|
|
1096
|
+
return this.subscriptions.has(id2);
|
|
1097
|
+
}
|
|
1098
|
+
remove(id2) {
|
|
1099
|
+
const sub = this.subscriptions.get(id2);
|
|
1100
|
+
if (!sub)
|
|
1101
|
+
return;
|
|
1102
|
+
this.subscriptions.delete(id2);
|
|
1103
|
+
const entityKey = `${sub.entity}:${sub.entityId}`;
|
|
1104
|
+
const ids = this.entityIndex.get(entityKey);
|
|
1105
|
+
if (ids) {
|
|
1106
|
+
ids.delete(id2);
|
|
1107
|
+
if (ids.size === 0) {
|
|
1108
|
+
this.entityIndex.delete(entityKey);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
getByEntity(entity22, entityId) {
|
|
1113
|
+
const entityKey = `${entity22}:${entityId}`;
|
|
1114
|
+
const ids = this.entityIndex.get(entityKey);
|
|
1115
|
+
if (!ids)
|
|
1116
|
+
return [];
|
|
1117
|
+
const result = [];
|
|
1118
|
+
for (const id2 of ids) {
|
|
1119
|
+
const sub = this.subscriptions.get(id2);
|
|
1120
|
+
if (sub) {
|
|
1121
|
+
result.push(sub);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
return result;
|
|
1125
|
+
}
|
|
1126
|
+
updateVersion(id2, version, data) {
|
|
1127
|
+
const sub = this.subscriptions.get(id2);
|
|
1128
|
+
if (!sub)
|
|
1129
|
+
return;
|
|
1130
|
+
sub.version = version;
|
|
1131
|
+
sub.lastUpdateAt = Date.now();
|
|
1132
|
+
if (data !== undefined) {
|
|
1133
|
+
sub.lastData = data;
|
|
1134
|
+
sub.lastDataHash = hashEntityState(data);
|
|
1135
|
+
}
|
|
1136
|
+
if (sub.state === "pending" || sub.state === "reconnecting") {
|
|
1137
|
+
sub.state = "active";
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
updateData(id2, data) {
|
|
1141
|
+
const sub = this.subscriptions.get(id2);
|
|
1142
|
+
if (!sub)
|
|
1143
|
+
return;
|
|
1144
|
+
sub.lastData = data;
|
|
1145
|
+
sub.lastDataHash = hashEntityState(data);
|
|
1146
|
+
}
|
|
1147
|
+
getLastData(id2) {
|
|
1148
|
+
return this.subscriptions.get(id2)?.lastData ?? null;
|
|
1149
|
+
}
|
|
1150
|
+
getVersion(id2) {
|
|
1151
|
+
return this.subscriptions.get(id2)?.version ?? null;
|
|
1152
|
+
}
|
|
1153
|
+
markActive(id2) {
|
|
1154
|
+
const sub = this.subscriptions.get(id2);
|
|
1155
|
+
if (sub) {
|
|
1156
|
+
sub.state = "active";
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
markError(id2) {
|
|
1160
|
+
const sub = this.subscriptions.get(id2);
|
|
1161
|
+
if (sub) {
|
|
1162
|
+
sub.state = "error";
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
markAllReconnecting() {
|
|
1166
|
+
for (const sub of this.subscriptions.values()) {
|
|
1167
|
+
if (sub.state === "active") {
|
|
1168
|
+
sub.state = "reconnecting";
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
getByState(state) {
|
|
1173
|
+
const result = [];
|
|
1174
|
+
for (const sub of this.subscriptions.values()) {
|
|
1175
|
+
if (sub.state === state) {
|
|
1176
|
+
result.push(sub);
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
return result;
|
|
1180
|
+
}
|
|
1181
|
+
getAllForReconnect() {
|
|
1182
|
+
const result = [];
|
|
1183
|
+
for (const sub of this.subscriptions.values()) {
|
|
1184
|
+
if (sub.state === "reconnecting" || sub.state === "active") {
|
|
1185
|
+
const reconnectSub = {
|
|
1186
|
+
id: sub.id,
|
|
1187
|
+
entity: sub.entity,
|
|
1188
|
+
entityId: sub.entityId,
|
|
1189
|
+
fields: sub.fields,
|
|
1190
|
+
version: sub.version,
|
|
1191
|
+
input: sub.input
|
|
1192
|
+
};
|
|
1193
|
+
if (sub.lastDataHash) {
|
|
1194
|
+
reconnectSub.dataHash = sub.lastDataHash;
|
|
1195
|
+
}
|
|
1196
|
+
result.push(reconnectSub);
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
return result;
|
|
1200
|
+
}
|
|
1201
|
+
processReconnectResult(id2, version, data) {
|
|
1202
|
+
const sub = this.subscriptions.get(id2);
|
|
1203
|
+
if (!sub)
|
|
1204
|
+
return;
|
|
1205
|
+
sub.version = version;
|
|
1206
|
+
sub.state = "active";
|
|
1207
|
+
sub.lastUpdateAt = Date.now();
|
|
1208
|
+
if (data !== undefined) {
|
|
1209
|
+
sub.lastData = data;
|
|
1210
|
+
sub.lastDataHash = hashEntityState(data);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
getObserver(id2) {
|
|
1214
|
+
return this.subscriptions.get(id2)?.observer;
|
|
1215
|
+
}
|
|
1216
|
+
updateObserver(id2, observer) {
|
|
1217
|
+
const sub = this.subscriptions.get(id2);
|
|
1218
|
+
if (sub) {
|
|
1219
|
+
sub.observer = observer;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
notifyNext(id2, data) {
|
|
1223
|
+
const sub = this.subscriptions.get(id2);
|
|
1224
|
+
sub?.observer.next?.({ data, version: sub.version });
|
|
1225
|
+
}
|
|
1226
|
+
notifyError(id2, error) {
|
|
1227
|
+
this.subscriptions.get(id2)?.observer.error?.(error);
|
|
1228
|
+
}
|
|
1229
|
+
notifyAllReconnectingError(error) {
|
|
1230
|
+
for (const sub of this.subscriptions.values()) {
|
|
1231
|
+
if (sub.state === "reconnecting") {
|
|
1232
|
+
sub.observer.error?.(error);
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
get size() {
|
|
1237
|
+
return this.subscriptions.size;
|
|
1238
|
+
}
|
|
1239
|
+
getIds() {
|
|
1240
|
+
return Array.from(this.subscriptions.keys());
|
|
1241
|
+
}
|
|
1242
|
+
values() {
|
|
1243
|
+
return this.subscriptions.values();
|
|
1244
|
+
}
|
|
1245
|
+
getStats() {
|
|
1246
|
+
const byState = {
|
|
1247
|
+
pending: 0,
|
|
1248
|
+
active: 0,
|
|
1249
|
+
reconnecting: 0,
|
|
1250
|
+
error: 0
|
|
1251
|
+
};
|
|
1252
|
+
const byEntity = {};
|
|
1253
|
+
for (const sub of this.subscriptions.values()) {
|
|
1254
|
+
byState[sub.state]++;
|
|
1255
|
+
const entityKey = `${sub.entity}:${sub.entityId}`;
|
|
1256
|
+
byEntity[entityKey] = (byEntity[entityKey] ?? 0) + 1;
|
|
1257
|
+
}
|
|
1258
|
+
return {
|
|
1259
|
+
total: this.subscriptions.size,
|
|
1260
|
+
byState,
|
|
1261
|
+
byEntity
|
|
1262
|
+
};
|
|
1263
|
+
}
|
|
1264
|
+
clear() {
|
|
1265
|
+
for (const sub of this.subscriptions.values()) {
|
|
1266
|
+
sub.observer.complete?.();
|
|
1267
|
+
}
|
|
1268
|
+
this.subscriptions.clear();
|
|
1269
|
+
this.entityIndex.clear();
|
|
1270
|
+
}
|
|
1271
|
+
clearErrors() {
|
|
1272
|
+
const toRemove = [];
|
|
1273
|
+
for (const [id2, sub] of this.subscriptions) {
|
|
1274
|
+
if (sub.state === "error") {
|
|
1275
|
+
toRemove.push(id2);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
for (const id2 of toRemove) {
|
|
1279
|
+
this.remove(id2);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
function applyOps(state, ops) {
|
|
1284
|
+
let result = state;
|
|
1285
|
+
for (const op2 of ops) {
|
|
1286
|
+
result = applyOp(result, op2);
|
|
1287
|
+
}
|
|
1288
|
+
return result;
|
|
1289
|
+
}
|
|
1290
|
+
function applyOp(state, op2) {
|
|
1291
|
+
switch (op2.o) {
|
|
1292
|
+
case "set":
|
|
1293
|
+
return setAtPath(state, op2.p, op2.v);
|
|
1294
|
+
case "del":
|
|
1295
|
+
return deleteAtPath(state, op2.p);
|
|
1296
|
+
case "merge":
|
|
1297
|
+
return mergeAtPath(state, op2.p, op2.v);
|
|
1298
|
+
case "delta": {
|
|
1299
|
+
const current = getAtPath(state, op2.p);
|
|
1300
|
+
const updated = applyUpdate(current, { strategy: "delta", data: op2.d });
|
|
1301
|
+
return setAtPath(state, op2.p, updated);
|
|
1302
|
+
}
|
|
1303
|
+
case "patch": {
|
|
1304
|
+
const current = getAtPath(state, op2.p);
|
|
1305
|
+
const updated = applyUpdate(current, { strategy: "patch", data: op2.d });
|
|
1306
|
+
return setAtPath(state, op2.p, updated);
|
|
1307
|
+
}
|
|
1308
|
+
case "push":
|
|
1309
|
+
return arrayPush(state, op2.p, op2.v);
|
|
1310
|
+
case "unshift":
|
|
1311
|
+
return arrayUnshift(state, op2.p, op2.v);
|
|
1312
|
+
case "splice":
|
|
1313
|
+
return arraySplice(state, op2.p, op2.i, op2.dc, op2.v);
|
|
1314
|
+
case "arrSet":
|
|
1315
|
+
return arraySetAt(state, op2.p, op2.i, op2.v);
|
|
1316
|
+
case "arrDel":
|
|
1317
|
+
return arrayDeleteAt(state, op2.p, op2.i);
|
|
1318
|
+
case "arrSetId":
|
|
1319
|
+
return arraySetById(state, op2.p, op2.id, op2.v);
|
|
1320
|
+
case "arrDelId":
|
|
1321
|
+
return arrayDeleteById(state, op2.p, op2.id);
|
|
1322
|
+
case "arrMerge":
|
|
1323
|
+
return arrayMergeAt(state, op2.p, op2.i, op2.v);
|
|
1324
|
+
case "arrMergeId":
|
|
1325
|
+
return arrayMergeById(state, op2.p, op2.id, op2.v);
|
|
1326
|
+
default:
|
|
1327
|
+
return state;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
function parsePath(path) {
|
|
1331
|
+
if (!path)
|
|
1332
|
+
return [];
|
|
1333
|
+
return path.split(".");
|
|
1334
|
+
}
|
|
1335
|
+
function getAtPath(state, path) {
|
|
1336
|
+
const segments = parsePath(path);
|
|
1337
|
+
let current = state;
|
|
1338
|
+
for (const segment of segments) {
|
|
1339
|
+
if (current === null || current === undefined)
|
|
1340
|
+
return;
|
|
1341
|
+
if (typeof current !== "object")
|
|
1342
|
+
return;
|
|
1343
|
+
current = current[segment];
|
|
1344
|
+
}
|
|
1345
|
+
return current;
|
|
1346
|
+
}
|
|
1347
|
+
function setAtPath(state, path, value) {
|
|
1348
|
+
const segments = parsePath(path);
|
|
1349
|
+
if (segments.length === 0)
|
|
1350
|
+
return value;
|
|
1351
|
+
return updateAtPath(state, segments, () => value);
|
|
1352
|
+
}
|
|
1353
|
+
function deleteAtPath(state, path) {
|
|
1354
|
+
const segments = parsePath(path);
|
|
1355
|
+
if (segments.length === 0)
|
|
1356
|
+
return;
|
|
1357
|
+
const parentPath = segments.slice(0, -1);
|
|
1358
|
+
const key = segments[segments.length - 1];
|
|
1359
|
+
return updateAtPath(state, parentPath, (parent) => {
|
|
1360
|
+
if (parent === null || parent === undefined)
|
|
1361
|
+
return parent;
|
|
1362
|
+
if (Array.isArray(parent)) {
|
|
1363
|
+
const idx = parseInt(key, 10);
|
|
1364
|
+
const result = [...parent];
|
|
1365
|
+
result.splice(idx, 1);
|
|
1366
|
+
return result;
|
|
1367
|
+
}
|
|
1368
|
+
if (typeof parent === "object") {
|
|
1369
|
+
const { [key]: _, ...rest } = parent;
|
|
1370
|
+
return rest;
|
|
1371
|
+
}
|
|
1372
|
+
return parent;
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
function mergeAtPath(state, path, value) {
|
|
1376
|
+
const segments = parsePath(path);
|
|
1377
|
+
return updateAtPath(state, segments, (current) => {
|
|
1378
|
+
if (current === null || current === undefined)
|
|
1379
|
+
return value;
|
|
1380
|
+
if (typeof current !== "object" || Array.isArray(current))
|
|
1381
|
+
return value;
|
|
1382
|
+
return { ...current, ...value };
|
|
1383
|
+
});
|
|
1384
|
+
}
|
|
1385
|
+
function updateAtPath(state, segments, transform) {
|
|
1386
|
+
if (segments.length === 0) {
|
|
1387
|
+
return transform(state);
|
|
1388
|
+
}
|
|
1389
|
+
const [head, ...tail] = segments;
|
|
1390
|
+
if (state === null || state === undefined) {
|
|
1391
|
+
const isArrayIndex = /^\d+$/.test(head);
|
|
1392
|
+
const newState = isArrayIndex ? [] : {};
|
|
1393
|
+
return updateAtPath(newState, segments, transform);
|
|
1394
|
+
}
|
|
1395
|
+
if (Array.isArray(state)) {
|
|
1396
|
+
const result = [...state];
|
|
1397
|
+
const idx = parseInt(head, 10);
|
|
1398
|
+
result[idx] = updateAtPath(result[idx], tail, transform);
|
|
1399
|
+
return result;
|
|
1400
|
+
}
|
|
1401
|
+
if (typeof state === "object") {
|
|
1402
|
+
const obj = state;
|
|
1403
|
+
return {
|
|
1404
|
+
...obj,
|
|
1405
|
+
[head]: updateAtPath(obj[head], tail, transform)
|
|
1406
|
+
};
|
|
1407
|
+
}
|
|
1408
|
+
return { [head]: updateAtPath(undefined, tail, transform) };
|
|
1409
|
+
}
|
|
1410
|
+
function arrayPush(state, path, items) {
|
|
1411
|
+
return updateAtPath(state, parsePath(path), (arr) => {
|
|
1412
|
+
if (!Array.isArray(arr))
|
|
1413
|
+
return items;
|
|
1414
|
+
return [...arr, ...items];
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
function arrayUnshift(state, path, items) {
|
|
1418
|
+
return updateAtPath(state, parsePath(path), (arr) => {
|
|
1419
|
+
if (!Array.isArray(arr))
|
|
1420
|
+
return items;
|
|
1421
|
+
return [...items, ...arr];
|
|
1422
|
+
});
|
|
1423
|
+
}
|
|
1424
|
+
function arraySplice(state, path, index, deleteCount, items) {
|
|
1425
|
+
return updateAtPath(state, parsePath(path), (arr) => {
|
|
1426
|
+
if (!Array.isArray(arr))
|
|
1427
|
+
return items ?? [];
|
|
1428
|
+
const result = [...arr];
|
|
1429
|
+
if (items) {
|
|
1430
|
+
result.splice(index, deleteCount, ...items);
|
|
1431
|
+
} else {
|
|
1432
|
+
result.splice(index, deleteCount);
|
|
1433
|
+
}
|
|
1434
|
+
return result;
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
function arraySetAt(state, path, index, value) {
|
|
1438
|
+
return updateAtPath(state, parsePath(path), (arr) => {
|
|
1439
|
+
if (!Array.isArray(arr)) {
|
|
1440
|
+
const result2 = [];
|
|
1441
|
+
result2[index] = value;
|
|
1442
|
+
return result2;
|
|
1443
|
+
}
|
|
1444
|
+
const result = [...arr];
|
|
1445
|
+
result[index] = value;
|
|
1446
|
+
return result;
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
function arrayDeleteAt(state, path, index) {
|
|
1450
|
+
return updateAtPath(state, parsePath(path), (arr) => {
|
|
1451
|
+
if (!Array.isArray(arr))
|
|
1452
|
+
return [];
|
|
1453
|
+
const result = [...arr];
|
|
1454
|
+
result.splice(index, 1);
|
|
1455
|
+
return result;
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
function arraySetById(state, path, id2, value) {
|
|
1459
|
+
return updateAtPath(state, parsePath(path), (arr) => {
|
|
1460
|
+
if (!Array.isArray(arr))
|
|
1461
|
+
return [value];
|
|
1462
|
+
const index = arr.findIndex((item) => item && typeof item === "object" && item.id === id2);
|
|
1463
|
+
if (index === -1) {
|
|
1464
|
+
return [...arr, value];
|
|
1465
|
+
}
|
|
1466
|
+
const result = [...arr];
|
|
1467
|
+
result[index] = value;
|
|
1468
|
+
return result;
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
function arrayDeleteById(state, path, id2) {
|
|
1472
|
+
return updateAtPath(state, parsePath(path), (arr) => {
|
|
1473
|
+
if (!Array.isArray(arr))
|
|
1474
|
+
return [];
|
|
1475
|
+
return arr.filter((item) => !(item && typeof item === "object" && item.id === id2));
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
function arrayMergeAt(state, path, index, value) {
|
|
1479
|
+
return updateAtPath(state, parsePath(path), (arr) => {
|
|
1480
|
+
if (!Array.isArray(arr)) {
|
|
1481
|
+
const result2 = [];
|
|
1482
|
+
result2[index] = value;
|
|
1483
|
+
return result2;
|
|
1484
|
+
}
|
|
1485
|
+
const result = [...arr];
|
|
1486
|
+
const current = result[index];
|
|
1487
|
+
if (current && typeof current === "object" && !Array.isArray(current)) {
|
|
1488
|
+
result[index] = { ...current, ...value };
|
|
1489
|
+
} else {
|
|
1490
|
+
result[index] = value;
|
|
1491
|
+
}
|
|
1492
|
+
return result;
|
|
1493
|
+
});
|
|
1494
|
+
}
|
|
1495
|
+
function arrayMergeById(state, path, id2, value) {
|
|
1496
|
+
return updateAtPath(state, parsePath(path), (arr) => {
|
|
1497
|
+
if (!Array.isArray(arr))
|
|
1498
|
+
return [{ id: id2, ...value }];
|
|
1499
|
+
const index = arr.findIndex((item) => item && typeof item === "object" && item.id === id2);
|
|
1500
|
+
if (index === -1) {
|
|
1501
|
+
return [...arr, { id: id2, ...value }];
|
|
1502
|
+
}
|
|
1503
|
+
const result = [...arr];
|
|
1504
|
+
const current = result[index];
|
|
1505
|
+
if (current && typeof current === "object" && !Array.isArray(current)) {
|
|
1506
|
+
result[index] = { ...current, ...value };
|
|
1507
|
+
} else {
|
|
1508
|
+
result[index] = { id: id2, ...value };
|
|
1509
|
+
}
|
|
1510
|
+
return result;
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
function isSnapshot(msg) {
|
|
1514
|
+
return msg.$ === "snapshot";
|
|
1515
|
+
}
|
|
1516
|
+
function isOps(msg) {
|
|
1517
|
+
return msg.$ === "ops";
|
|
1518
|
+
}
|
|
1519
|
+
function isError(msg) {
|
|
1520
|
+
return msg.$ === "error";
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
// ../client/dist/index.js
|
|
1524
|
+
class SelectionRegistry {
|
|
1525
|
+
endpoints = new Map;
|
|
1526
|
+
addSubscriber(params) {
|
|
1527
|
+
const { endpointKey, subscriberId, selection, onData, onError } = params;
|
|
1528
|
+
let endpoint = this.endpoints.get(endpointKey);
|
|
1529
|
+
const previousSelection = endpoint ? { ...endpoint.mergedSelection } : {};
|
|
1530
|
+
if (!endpoint) {
|
|
1531
|
+
endpoint = {
|
|
1532
|
+
key: endpointKey,
|
|
1533
|
+
subscribers: new Map,
|
|
1534
|
+
mergedSelection: {},
|
|
1535
|
+
lastData: null,
|
|
1536
|
+
isSubscribed: false,
|
|
1537
|
+
createdAt: Date.now(),
|
|
1538
|
+
lastSelectionChangeAt: null
|
|
1539
|
+
};
|
|
1540
|
+
this.endpoints.set(endpointKey, endpoint);
|
|
1541
|
+
}
|
|
1542
|
+
const subscriberMeta = {
|
|
1543
|
+
id: subscriberId,
|
|
1544
|
+
selection,
|
|
1545
|
+
onData,
|
|
1546
|
+
createdAt: Date.now()
|
|
1547
|
+
};
|
|
1548
|
+
if (onError) {
|
|
1549
|
+
subscriberMeta.onError = onError;
|
|
1550
|
+
}
|
|
1551
|
+
endpoint.subscribers.set(subscriberId, subscriberMeta);
|
|
1552
|
+
const newSelection = this.computeMergedSelection(endpoint);
|
|
1553
|
+
const analysis = this.analyzeSelectionChange(previousSelection, newSelection);
|
|
1554
|
+
if (analysis.hasChanged) {
|
|
1555
|
+
endpoint.mergedSelection = newSelection;
|
|
1556
|
+
endpoint.lastSelectionChangeAt = Date.now();
|
|
1557
|
+
}
|
|
1558
|
+
return analysis;
|
|
1559
|
+
}
|
|
1560
|
+
removeSubscriber(endpointKey, subscriberId) {
|
|
1561
|
+
const endpoint = this.endpoints.get(endpointKey);
|
|
1562
|
+
if (!endpoint) {
|
|
1563
|
+
return this.noChangeAnalysis();
|
|
1564
|
+
}
|
|
1565
|
+
const previousSelection = { ...endpoint.mergedSelection };
|
|
1566
|
+
endpoint.subscribers.delete(subscriberId);
|
|
1567
|
+
if (endpoint.subscribers.size === 0) {
|
|
1568
|
+
this.endpoints.delete(endpointKey);
|
|
1569
|
+
return this.analyzeSelectionChange(previousSelection, {});
|
|
1570
|
+
}
|
|
1571
|
+
const newSelection = this.computeMergedSelection(endpoint);
|
|
1572
|
+
const analysis = this.analyzeSelectionChange(previousSelection, newSelection);
|
|
1573
|
+
if (analysis.hasChanged) {
|
|
1574
|
+
endpoint.mergedSelection = newSelection;
|
|
1575
|
+
endpoint.lastSelectionChangeAt = Date.now();
|
|
1576
|
+
}
|
|
1577
|
+
return analysis;
|
|
1578
|
+
}
|
|
1579
|
+
getMergedSelection(endpointKey) {
|
|
1580
|
+
return this.endpoints.get(endpointKey)?.mergedSelection ?? null;
|
|
1581
|
+
}
|
|
1582
|
+
getSubscriberIds(endpointKey) {
|
|
1583
|
+
const endpoint = this.endpoints.get(endpointKey);
|
|
1584
|
+
return endpoint ? Array.from(endpoint.subscribers.keys()) : [];
|
|
1585
|
+
}
|
|
1586
|
+
getSubscriberCount(endpointKey) {
|
|
1587
|
+
return this.endpoints.get(endpointKey)?.subscribers.size ?? 0;
|
|
1588
|
+
}
|
|
1589
|
+
hasSubscribers(endpointKey) {
|
|
1590
|
+
return (this.endpoints.get(endpointKey)?.subscribers.size ?? 0) > 0;
|
|
1591
|
+
}
|
|
1592
|
+
distributeData(endpointKey, data) {
|
|
1593
|
+
const endpoint = this.endpoints.get(endpointKey);
|
|
1594
|
+
if (!endpoint)
|
|
1595
|
+
return;
|
|
1596
|
+
endpoint.lastData = data;
|
|
1597
|
+
for (const subscriber of endpoint.subscribers.values()) {
|
|
1598
|
+
try {
|
|
1599
|
+
const filteredData = filterToSelection(data, subscriber.selection);
|
|
1600
|
+
subscriber.onData(filteredData);
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
if (subscriber.onError) {
|
|
1603
|
+
subscriber.onError(error instanceof Error ? error : new Error(String(error)));
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
distributeError(endpointKey, error) {
|
|
1609
|
+
const endpoint = this.endpoints.get(endpointKey);
|
|
1610
|
+
if (!endpoint)
|
|
1611
|
+
return;
|
|
1612
|
+
for (const subscriber of endpoint.subscribers.values()) {
|
|
1613
|
+
subscriber.onError?.(error);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
getLastData(endpointKey) {
|
|
1617
|
+
return this.endpoints.get(endpointKey)?.lastData ?? null;
|
|
1618
|
+
}
|
|
1619
|
+
markSubscribed(endpointKey) {
|
|
1620
|
+
const endpoint = this.endpoints.get(endpointKey);
|
|
1621
|
+
if (endpoint) {
|
|
1622
|
+
endpoint.isSubscribed = true;
|
|
1623
|
+
}
|
|
1624
|
+
}
|
|
1625
|
+
markUnsubscribed(endpointKey) {
|
|
1626
|
+
const endpoint = this.endpoints.get(endpointKey);
|
|
1627
|
+
if (endpoint) {
|
|
1628
|
+
endpoint.isSubscribed = false;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
isSubscribed(endpointKey) {
|
|
1632
|
+
return this.endpoints.get(endpointKey)?.isSubscribed ?? false;
|
|
1633
|
+
}
|
|
1634
|
+
getEndpointKeys() {
|
|
1635
|
+
return Array.from(this.endpoints.keys());
|
|
1636
|
+
}
|
|
1637
|
+
clear() {
|
|
1638
|
+
this.endpoints.clear();
|
|
1639
|
+
}
|
|
1640
|
+
getStats() {
|
|
1641
|
+
let totalSubscribers = 0;
|
|
1642
|
+
for (const endpoint of this.endpoints.values()) {
|
|
1643
|
+
totalSubscribers += endpoint.subscribers.size;
|
|
1644
|
+
}
|
|
1645
|
+
return {
|
|
1646
|
+
endpointCount: this.endpoints.size,
|
|
1647
|
+
totalSubscribers,
|
|
1648
|
+
avgSubscribersPerEndpoint: this.endpoints.size > 0 ? totalSubscribers / this.endpoints.size : 0
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1651
|
+
computeMergedSelection(endpoint) {
|
|
1652
|
+
const selections = Array.from(endpoint.subscribers.values()).map((s) => s.selection);
|
|
1653
|
+
return mergeSelections(selections);
|
|
1654
|
+
}
|
|
1655
|
+
analyzeSelectionChange(previous, next) {
|
|
1656
|
+
const previousFields = this.flattenSelectionKeys(previous);
|
|
1657
|
+
const nextFields = this.flattenSelectionKeys(next);
|
|
1658
|
+
const addedFields = new Set;
|
|
1659
|
+
const removedFields = new Set;
|
|
1660
|
+
for (const field of nextFields) {
|
|
1661
|
+
if (!previousFields.has(field)) {
|
|
1662
|
+
addedFields.add(field);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
for (const field of previousFields) {
|
|
1666
|
+
if (!nextFields.has(field)) {
|
|
1667
|
+
removedFields.add(field);
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
const hasChanged = addedFields.size > 0 || removedFields.size > 0;
|
|
1671
|
+
return {
|
|
1672
|
+
hasChanged,
|
|
1673
|
+
previousSelection: previous,
|
|
1674
|
+
newSelection: next,
|
|
1675
|
+
addedFields,
|
|
1676
|
+
removedFields,
|
|
1677
|
+
isExpanded: addedFields.size > 0,
|
|
1678
|
+
isShrunk: removedFields.size > 0
|
|
1679
|
+
};
|
|
1680
|
+
}
|
|
1681
|
+
flattenSelectionKeys(selection, prefix = "") {
|
|
1682
|
+
const keys = new Set;
|
|
1683
|
+
for (const [key, value] of Object.entries(selection)) {
|
|
1684
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
1685
|
+
keys.add(path);
|
|
1686
|
+
if (typeof value === "boolean") {
|
|
1687
|
+
continue;
|
|
1688
|
+
}
|
|
1689
|
+
if (typeof value === "object" && value !== null) {
|
|
1690
|
+
let nestedSelection;
|
|
1691
|
+
if ("select" in value && typeof value.select === "object") {
|
|
1692
|
+
nestedSelection = value.select;
|
|
1693
|
+
} else if (!("input" in value)) {
|
|
1694
|
+
nestedSelection = value;
|
|
1695
|
+
}
|
|
1696
|
+
if (nestedSelection) {
|
|
1697
|
+
const nestedKeys = this.flattenSelectionKeys(nestedSelection, path);
|
|
1698
|
+
for (const nestedKey of nestedKeys) {
|
|
1699
|
+
keys.add(nestedKey);
|
|
1700
|
+
}
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
return keys;
|
|
1705
|
+
}
|
|
1706
|
+
noChangeAnalysis() {
|
|
1707
|
+
return {
|
|
1708
|
+
hasChanged: false,
|
|
1709
|
+
previousSelection: {},
|
|
1710
|
+
newSelection: {},
|
|
1711
|
+
addedFields: new Set,
|
|
1712
|
+
removedFields: new Set,
|
|
1713
|
+
isExpanded: false,
|
|
1714
|
+
isShrunk: false
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
function mergeSelections(selections) {
|
|
1719
|
+
if (selections.length === 0)
|
|
1720
|
+
return {};
|
|
1721
|
+
if (selections.length === 1)
|
|
1722
|
+
return selections[0];
|
|
1723
|
+
const merged = {};
|
|
1724
|
+
const allKeys = new Set;
|
|
1725
|
+
for (const selection of selections) {
|
|
1726
|
+
for (const key of Object.keys(selection)) {
|
|
1727
|
+
allKeys.add(key);
|
|
1728
|
+
}
|
|
1729
|
+
}
|
|
1730
|
+
for (const key of allKeys) {
|
|
1731
|
+
const values = selections.map((s) => s[key]).filter((v) => v !== undefined && v !== null);
|
|
1732
|
+
if (values.length === 0)
|
|
1733
|
+
continue;
|
|
1734
|
+
if (values.some((v) => v === true)) {
|
|
1735
|
+
merged[key] = true;
|
|
1736
|
+
continue;
|
|
1737
|
+
}
|
|
1738
|
+
const nestedSelections = [];
|
|
1739
|
+
let lastInput;
|
|
1740
|
+
for (const value of values) {
|
|
1741
|
+
if (typeof value === "object" && value !== null) {
|
|
1742
|
+
if ("select" in value && typeof value.select === "object") {
|
|
1743
|
+
nestedSelections.push(value.select);
|
|
1744
|
+
if ("input" in value) {
|
|
1745
|
+
lastInput = value.input;
|
|
1746
|
+
}
|
|
1747
|
+
} else if ("input" in value) {
|
|
1748
|
+
lastInput = value.input;
|
|
1749
|
+
merged[key] = { input: lastInput };
|
|
1750
|
+
} else {
|
|
1751
|
+
nestedSelections.push(value);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
if (nestedSelections.length > 0) {
|
|
1756
|
+
const mergedNested = mergeSelections(nestedSelections);
|
|
1757
|
+
if (lastInput !== undefined) {
|
|
1758
|
+
merged[key] = { input: lastInput, select: mergedNested };
|
|
1759
|
+
} else {
|
|
1760
|
+
merged[key] = mergedNested;
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
return merged;
|
|
1765
|
+
}
|
|
1766
|
+
function filterToSelection(data, selection) {
|
|
1767
|
+
if (data == null)
|
|
1768
|
+
return data;
|
|
1769
|
+
if (Array.isArray(data)) {
|
|
1770
|
+
return data.map((item) => filterToSelection(item, selection));
|
|
1771
|
+
}
|
|
1772
|
+
if (typeof data !== "object")
|
|
1773
|
+
return data;
|
|
1774
|
+
const obj = data;
|
|
1775
|
+
const result = {};
|
|
1776
|
+
if ("id" in obj) {
|
|
1777
|
+
result.id = obj.id;
|
|
1778
|
+
}
|
|
1779
|
+
for (const [key, value] of Object.entries(selection)) {
|
|
1780
|
+
if (!(key in obj))
|
|
1781
|
+
continue;
|
|
1782
|
+
if (value === true) {
|
|
1783
|
+
result[key] = obj[key];
|
|
1784
|
+
} else if (typeof value === "object" && value !== null) {
|
|
1785
|
+
let nestedSelection = null;
|
|
1786
|
+
if ("select" in value && typeof value.select === "object") {
|
|
1787
|
+
nestedSelection = value.select;
|
|
1788
|
+
} else if (!("input" in value)) {
|
|
1789
|
+
nestedSelection = value;
|
|
1790
|
+
}
|
|
1791
|
+
if (nestedSelection) {
|
|
1792
|
+
result[key] = filterToSelection(obj[key], nestedSelection);
|
|
1793
|
+
} else {
|
|
1794
|
+
result[key] = obj[key];
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
847
1797
|
}
|
|
1798
|
+
return result;
|
|
848
1799
|
}
|
|
849
|
-
|
|
850
|
-
// ../client/dist/index.js
|
|
851
1800
|
function hasAnySubscription(entities, entityName, select, visited = new Set) {
|
|
852
1801
|
if (!entities)
|
|
853
1802
|
return false;
|
|
@@ -879,13 +1828,19 @@ function hasAnySubscription(entities, entityName, select, visited = new Set) {
|
|
|
879
1828
|
}
|
|
880
1829
|
return false;
|
|
881
1830
|
}
|
|
1831
|
+
var subscriberIdCounter = 0;
|
|
1832
|
+
function generateSubscriberId() {
|
|
1833
|
+
return `sub_${Date.now()}_${++subscriberIdCounter}`;
|
|
1834
|
+
}
|
|
882
1835
|
|
|
883
1836
|
class ClientImpl {
|
|
884
1837
|
transport;
|
|
885
1838
|
plugins;
|
|
886
1839
|
metadata = null;
|
|
887
1840
|
connectPromise = null;
|
|
888
|
-
|
|
1841
|
+
endpoints = new Map;
|
|
1842
|
+
pendingBatches = new Map;
|
|
1843
|
+
batchScheduled = false;
|
|
889
1844
|
queryResultCache = new Map;
|
|
890
1845
|
observerEntries = new WeakMap;
|
|
891
1846
|
constructor(config) {
|
|
@@ -926,16 +1881,16 @@ class ClientImpl {
|
|
|
926
1881
|
result = await plugin.afterResponse(result, processedOp);
|
|
927
1882
|
}
|
|
928
1883
|
}
|
|
929
|
-
if (result
|
|
930
|
-
const error = result.error;
|
|
1884
|
+
if (isError(result)) {
|
|
1885
|
+
const error = new Error(result.error);
|
|
931
1886
|
for (const plugin of this.plugins) {
|
|
932
1887
|
if (plugin.onError) {
|
|
933
1888
|
try {
|
|
934
1889
|
result = await plugin.onError(error, processedOp, () => this.execute(processedOp));
|
|
935
|
-
if (!result
|
|
1890
|
+
if (!isError(result))
|
|
936
1891
|
break;
|
|
937
1892
|
} catch (e) {
|
|
938
|
-
result = { error: e };
|
|
1893
|
+
result = { $: "error", error: e instanceof Error ? e.message : String(e) };
|
|
939
1894
|
}
|
|
940
1895
|
}
|
|
941
1896
|
}
|
|
@@ -1001,7 +1956,7 @@ class ClientImpl {
|
|
|
1001
1956
|
return `${type}-${path}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
1002
1957
|
}
|
|
1003
1958
|
inputHashCache = new WeakMap;
|
|
1004
|
-
|
|
1959
|
+
makeEndpointKey(path, input) {
|
|
1005
1960
|
if (input === undefined || input === null) {
|
|
1006
1961
|
return `${path}:null`;
|
|
1007
1962
|
}
|
|
@@ -1016,69 +1971,231 @@ class ClientImpl {
|
|
|
1016
1971
|
}
|
|
1017
1972
|
return `${path}:${hash}`;
|
|
1018
1973
|
}
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1974
|
+
makeQueryResultKey(endpointKey, select) {
|
|
1975
|
+
if (!select)
|
|
1976
|
+
return endpointKey;
|
|
1977
|
+
return `${endpointKey}:${JSON.stringify(select)}`;
|
|
1978
|
+
}
|
|
1979
|
+
getOrCreateEndpoint(key) {
|
|
1980
|
+
let endpoint = this.endpoints.get(key);
|
|
1981
|
+
if (!endpoint) {
|
|
1982
|
+
endpoint = {
|
|
1027
1983
|
data: null,
|
|
1028
1984
|
error: null,
|
|
1029
1985
|
completed: false,
|
|
1030
|
-
observers: new
|
|
1031
|
-
|
|
1032
|
-
|
|
1986
|
+
observers: new Map,
|
|
1987
|
+
mergedSelection: undefined,
|
|
1988
|
+
isSubscribed: false
|
|
1989
|
+
};
|
|
1990
|
+
this.endpoints.set(key, endpoint);
|
|
1991
|
+
}
|
|
1992
|
+
return endpoint;
|
|
1993
|
+
}
|
|
1994
|
+
addObserver(key, observer) {
|
|
1995
|
+
const endpoint = this.getOrCreateEndpoint(key);
|
|
1996
|
+
const previousSelection = endpoint.mergedSelection;
|
|
1997
|
+
endpoint.observers.set(observer.id, observer);
|
|
1998
|
+
const selections = Array.from(endpoint.observers.values()).map((o) => o.selection).filter((s) => s !== undefined);
|
|
1999
|
+
endpoint.mergedSelection = selections.length > 0 ? mergeSelections(selections) : undefined;
|
|
2000
|
+
const selectionChanged = JSON.stringify(previousSelection) !== JSON.stringify(endpoint.mergedSelection);
|
|
2001
|
+
const isExpanded = selectionChanged && this.isSelectionExpanded(previousSelection, endpoint.mergedSelection);
|
|
2002
|
+
return { endpoint, selectionChanged, isExpanded };
|
|
2003
|
+
}
|
|
2004
|
+
removeObserver(key, observerId) {
|
|
2005
|
+
const endpoint = this.endpoints.get(key);
|
|
2006
|
+
if (!endpoint)
|
|
2007
|
+
return { endpoint: undefined, shouldUnsubscribe: false };
|
|
2008
|
+
endpoint.observers.delete(observerId);
|
|
2009
|
+
if (endpoint.observers.size === 0) {
|
|
2010
|
+
return { endpoint, shouldUnsubscribe: true };
|
|
2011
|
+
}
|
|
2012
|
+
const selections = Array.from(endpoint.observers.values()).map((o) => o.selection).filter((s) => s !== undefined);
|
|
2013
|
+
endpoint.mergedSelection = selections.length > 0 ? mergeSelections(selections) : undefined;
|
|
2014
|
+
return { endpoint, shouldUnsubscribe: false };
|
|
2015
|
+
}
|
|
2016
|
+
isSelectionExpanded(previous, current) {
|
|
2017
|
+
if (!previous)
|
|
2018
|
+
return current !== undefined;
|
|
2019
|
+
if (!current)
|
|
2020
|
+
return false;
|
|
2021
|
+
const previousKeys = this.flattenSelectionKeys(previous);
|
|
2022
|
+
const currentKeys = this.flattenSelectionKeys(current);
|
|
2023
|
+
for (const key of currentKeys) {
|
|
2024
|
+
if (!previousKeys.has(key))
|
|
2025
|
+
return true;
|
|
2026
|
+
}
|
|
2027
|
+
return false;
|
|
2028
|
+
}
|
|
2029
|
+
flattenSelectionKeys(selection, prefix = "") {
|
|
2030
|
+
const keys = new Set;
|
|
2031
|
+
for (const [key, value] of Object.entries(selection)) {
|
|
2032
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
2033
|
+
keys.add(path);
|
|
2034
|
+
if (typeof value === "boolean") {
|
|
2035
|
+
continue;
|
|
2036
|
+
}
|
|
2037
|
+
if (typeof value === "object" && value !== null) {
|
|
2038
|
+
const nested = "select" in value ? value.select : value;
|
|
2039
|
+
if (nested && typeof nested === "object") {
|
|
2040
|
+
for (const nestedKey of this.flattenSelectionKeys(nested, path)) {
|
|
2041
|
+
keys.add(nestedKey);
|
|
2042
|
+
}
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
}
|
|
2046
|
+
return keys;
|
|
2047
|
+
}
|
|
2048
|
+
distributeData(endpoint, data) {
|
|
2049
|
+
endpoint.data = data;
|
|
2050
|
+
endpoint.error = null;
|
|
2051
|
+
for (const observer of endpoint.observers.values()) {
|
|
2052
|
+
if (observer.next) {
|
|
2053
|
+
const filteredData = observer.selection ? filterToSelection(data, observer.selection) : data;
|
|
2054
|
+
observer.next(filteredData);
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
distributeError(endpoint, error) {
|
|
2059
|
+
endpoint.error = error;
|
|
2060
|
+
for (const observer of endpoint.observers.values()) {
|
|
2061
|
+
observer.error?.(error);
|
|
1033
2062
|
}
|
|
1034
|
-
|
|
2063
|
+
}
|
|
2064
|
+
scheduleBatchedQuery(key, path, input, selection) {
|
|
2065
|
+
return new Promise((resolve, reject) => {
|
|
2066
|
+
const observerId = generateSubscriberId();
|
|
2067
|
+
let batch = this.pendingBatches.get(key);
|
|
2068
|
+
if (!batch) {
|
|
2069
|
+
batch = {
|
|
2070
|
+
path,
|
|
2071
|
+
input,
|
|
2072
|
+
observers: [],
|
|
2073
|
+
mergedSelection: undefined
|
|
2074
|
+
};
|
|
2075
|
+
this.pendingBatches.set(key, batch);
|
|
2076
|
+
}
|
|
2077
|
+
batch.observers.push({ id: observerId, selection, resolve, reject });
|
|
2078
|
+
const selections = batch.observers.map((o) => o.selection).filter((s) => s !== undefined);
|
|
2079
|
+
batch.mergedSelection = selections.length > 0 ? mergeSelections(selections) : undefined;
|
|
2080
|
+
if (!this.batchScheduled) {
|
|
2081
|
+
this.batchScheduled = true;
|
|
2082
|
+
queueMicrotask(() => this.flushBatches());
|
|
2083
|
+
}
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
async flushBatches() {
|
|
2087
|
+
this.batchScheduled = false;
|
|
2088
|
+
const batches = Array.from(this.pendingBatches.entries());
|
|
2089
|
+
this.pendingBatches.clear();
|
|
2090
|
+
await Promise.all(batches.map(async ([key, batch]) => {
|
|
2091
|
+
try {
|
|
2092
|
+
const op2 = {
|
|
2093
|
+
id: this.generateId("query", batch.path),
|
|
2094
|
+
path: batch.path,
|
|
2095
|
+
type: "query",
|
|
2096
|
+
input: batch.input,
|
|
2097
|
+
meta: batch.mergedSelection ? { select: batch.mergedSelection } : {}
|
|
2098
|
+
};
|
|
2099
|
+
const response = await this.execute(op2);
|
|
2100
|
+
if (isError(response)) {
|
|
2101
|
+
const error = new Error(response.error);
|
|
2102
|
+
for (const observer of batch.observers) {
|
|
2103
|
+
observer.reject(error);
|
|
2104
|
+
}
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
if (isSnapshot(response)) {
|
|
2108
|
+
const endpoint = this.getOrCreateEndpoint(key);
|
|
2109
|
+
endpoint.data = response.data;
|
|
2110
|
+
for (const observer of batch.observers) {
|
|
2111
|
+
const filteredData = observer.selection ? filterToSelection(response.data, observer.selection) : response.data;
|
|
2112
|
+
observer.resolve(filteredData);
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
} catch (error) {
|
|
2116
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2117
|
+
for (const observer of batch.observers) {
|
|
2118
|
+
observer.reject(err);
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
}));
|
|
2122
|
+
}
|
|
2123
|
+
executeQuery(path, input, select) {
|
|
2124
|
+
const key = this.makeEndpointKey(path, input);
|
|
2125
|
+
const cacheKey = this.makeQueryResultKey(key, select);
|
|
2126
|
+
const cached = this.queryResultCache.get(cacheKey);
|
|
2127
|
+
if (cached) {
|
|
2128
|
+
return cached;
|
|
2129
|
+
}
|
|
2130
|
+
const endpoint = this.getOrCreateEndpoint(key);
|
|
1035
2131
|
const result = {
|
|
1036
2132
|
get value() {
|
|
1037
|
-
|
|
2133
|
+
if (endpoint.data === null)
|
|
2134
|
+
return null;
|
|
2135
|
+
return select ? filterToSelection(endpoint.data, select) : endpoint.data;
|
|
1038
2136
|
},
|
|
1039
2137
|
subscribe: (observerOrCallback) => {
|
|
2138
|
+
const observerId = generateSubscriberId();
|
|
1040
2139
|
let entry;
|
|
1041
2140
|
if (typeof observerOrCallback === "function") {
|
|
1042
2141
|
const callback = observerOrCallback;
|
|
1043
|
-
entry = {
|
|
2142
|
+
entry = {
|
|
2143
|
+
id: observerId,
|
|
2144
|
+
selection: select,
|
|
2145
|
+
next: (data) => callback(data)
|
|
2146
|
+
};
|
|
1044
2147
|
} else if (observerOrCallback && typeof observerOrCallback === "object") {
|
|
1045
2148
|
const observer = observerOrCallback;
|
|
1046
2149
|
entry = {
|
|
2150
|
+
id: observerId,
|
|
2151
|
+
selection: select,
|
|
1047
2152
|
next: observer.next ? (data) => observer.next(data) : undefined,
|
|
1048
2153
|
error: observer.error,
|
|
1049
2154
|
complete: observer.complete
|
|
1050
2155
|
};
|
|
1051
2156
|
} else {
|
|
1052
|
-
entry = {};
|
|
2157
|
+
entry = { id: observerId, selection: select };
|
|
1053
2158
|
}
|
|
1054
2159
|
if (observerOrCallback) {
|
|
1055
2160
|
this.observerEntries.set(observerOrCallback, entry);
|
|
1056
2161
|
}
|
|
1057
|
-
|
|
1058
|
-
if (
|
|
1059
|
-
entry.error(sub.error);
|
|
1060
|
-
} else if (sub.data !== null && entry.next) {
|
|
1061
|
-
entry.next(sub.data);
|
|
1062
|
-
}
|
|
1063
|
-
if (sub.completed && entry.complete) {
|
|
1064
|
-
entry.complete();
|
|
1065
|
-
}
|
|
1066
|
-
if (!sub.unsubscribe) {
|
|
2162
|
+
const { endpoint: ep, isExpanded } = this.addObserver(key, entry);
|
|
2163
|
+
if (!ep.isSubscribed) {
|
|
1067
2164
|
this.startSubscription(path, input, key);
|
|
2165
|
+
} else if (isExpanded) {
|
|
2166
|
+
if (ep.unsubscribe) {
|
|
2167
|
+
ep.unsubscribe();
|
|
2168
|
+
}
|
|
2169
|
+
ep.isSubscribed = false;
|
|
2170
|
+
this.startSubscription(path, input, key);
|
|
2171
|
+
} else {
|
|
2172
|
+
if (ep.error && entry.error) {
|
|
2173
|
+
entry.error(ep.error);
|
|
2174
|
+
} else if (ep.data !== null && entry.next) {
|
|
2175
|
+
const filteredData = select ? filterToSelection(ep.data, select) : ep.data;
|
|
2176
|
+
entry.next(filteredData);
|
|
2177
|
+
}
|
|
2178
|
+
if (ep.completed && entry.complete) {
|
|
2179
|
+
entry.complete();
|
|
2180
|
+
}
|
|
1068
2181
|
}
|
|
1069
2182
|
return () => {
|
|
1070
2183
|
if (observerOrCallback) {
|
|
1071
2184
|
const storedEntry = this.observerEntries.get(observerOrCallback);
|
|
1072
2185
|
if (storedEntry) {
|
|
1073
|
-
|
|
2186
|
+
const { shouldUnsubscribe } = this.removeObserver(key, storedEntry.id);
|
|
2187
|
+
if (shouldUnsubscribe) {
|
|
2188
|
+
ep.unsubscribe?.();
|
|
2189
|
+
ep.isSubscribed = false;
|
|
2190
|
+
this.endpoints.delete(key);
|
|
2191
|
+
for (const [k] of this.queryResultCache) {
|
|
2192
|
+
if (k.startsWith(key)) {
|
|
2193
|
+
this.queryResultCache.delete(k);
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
1074
2197
|
}
|
|
1075
2198
|
}
|
|
1076
|
-
if (sub.observers.size === 0 && sub.unsubscribe) {
|
|
1077
|
-
sub.unsubscribe();
|
|
1078
|
-
sub.unsubscribe = undefined;
|
|
1079
|
-
this.subscriptions.delete(key);
|
|
1080
|
-
this.queryResultCache.delete(key);
|
|
1081
|
-
}
|
|
1082
2199
|
};
|
|
1083
2200
|
},
|
|
1084
2201
|
select: (selection) => {
|
|
@@ -1086,27 +2203,17 @@ class ClientImpl {
|
|
|
1086
2203
|
},
|
|
1087
2204
|
then: async (onfulfilled, onrejected) => {
|
|
1088
2205
|
try {
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
input,
|
|
1094
|
-
meta: select ? { select } : {}
|
|
1095
|
-
};
|
|
1096
|
-
const response = await this.execute(op2);
|
|
1097
|
-
if (response.error) {
|
|
1098
|
-
throw response.error;
|
|
2206
|
+
const data = await this.scheduleBatchedQuery(key, path, input, select);
|
|
2207
|
+
const ep = this.getOrCreateEndpoint(key);
|
|
2208
|
+
if (ep.data === null) {
|
|
2209
|
+
ep.data = data;
|
|
1099
2210
|
}
|
|
1100
|
-
|
|
1101
|
-
for (const observer of sub.observers) {
|
|
1102
|
-
observer.next?.(response.data);
|
|
1103
|
-
}
|
|
1104
|
-
return onfulfilled ? onfulfilled(response.data) : response.data;
|
|
2211
|
+
return onfulfilled ? onfulfilled(data) : data;
|
|
1105
2212
|
} catch (error) {
|
|
1106
2213
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
2214
|
+
const ep = this.endpoints.get(key);
|
|
2215
|
+
if (ep) {
|
|
2216
|
+
ep.error = err;
|
|
1110
2217
|
}
|
|
1111
2218
|
if (onrejected) {
|
|
1112
2219
|
return onrejected(error);
|
|
@@ -1115,65 +2222,70 @@ class ClientImpl {
|
|
|
1115
2222
|
}
|
|
1116
2223
|
}
|
|
1117
2224
|
};
|
|
1118
|
-
|
|
1119
|
-
this.queryResultCache.set(key, result);
|
|
1120
|
-
}
|
|
2225
|
+
this.queryResultCache.set(cacheKey, result);
|
|
1121
2226
|
return result;
|
|
1122
2227
|
}
|
|
1123
2228
|
async startSubscription(path, input, key) {
|
|
1124
|
-
const
|
|
1125
|
-
if (!
|
|
2229
|
+
const endpoint = this.endpoints.get(key);
|
|
2230
|
+
if (!endpoint)
|
|
1126
2231
|
return;
|
|
1127
2232
|
await this.ensureConnected();
|
|
1128
|
-
const
|
|
2233
|
+
const meta = this.getOperationMeta(path);
|
|
2234
|
+
if (meta?.type === "mutation") {
|
|
2235
|
+
return;
|
|
2236
|
+
}
|
|
2237
|
+
const isSubscription = this.requiresSubscription(path, endpoint.mergedSelection);
|
|
2238
|
+
endpoint.isSubscribed = true;
|
|
1129
2239
|
if (isSubscription) {
|
|
1130
2240
|
const op2 = {
|
|
1131
2241
|
id: this.generateId("subscription", path),
|
|
1132
2242
|
path,
|
|
1133
2243
|
type: "subscription",
|
|
1134
|
-
input
|
|
2244
|
+
input,
|
|
2245
|
+
meta: endpoint.mergedSelection ? { select: endpoint.mergedSelection } : {}
|
|
1135
2246
|
};
|
|
1136
2247
|
const resultOrObservable = this.transport.execute(op2);
|
|
1137
2248
|
if (this.isObservable(resultOrObservable)) {
|
|
1138
2249
|
const subscription = resultOrObservable.subscribe({
|
|
1139
|
-
next: (
|
|
1140
|
-
if (
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
sub.error = err;
|
|
1150
|
-
for (const observer of sub.observers) {
|
|
1151
|
-
observer.error?.(err);
|
|
2250
|
+
next: (message) => {
|
|
2251
|
+
if (isSnapshot(message)) {
|
|
2252
|
+
this.distributeData(endpoint, message.data);
|
|
2253
|
+
} else if (isOps(message)) {
|
|
2254
|
+
try {
|
|
2255
|
+
const newData = applyOps(endpoint.data, message.ops);
|
|
2256
|
+
this.distributeData(endpoint, newData);
|
|
2257
|
+
} catch (updateErr) {
|
|
2258
|
+
const err = updateErr instanceof Error ? updateErr : new Error(String(updateErr));
|
|
2259
|
+
this.distributeError(endpoint, err);
|
|
1152
2260
|
}
|
|
2261
|
+
} else if (isError(message)) {
|
|
2262
|
+
this.distributeError(endpoint, new Error(message.error));
|
|
1153
2263
|
}
|
|
1154
2264
|
},
|
|
1155
2265
|
error: (err) => {
|
|
1156
|
-
|
|
1157
|
-
for (const observer of sub.observers) {
|
|
1158
|
-
observer.error?.(err);
|
|
1159
|
-
}
|
|
2266
|
+
this.distributeError(endpoint, err);
|
|
1160
2267
|
},
|
|
1161
2268
|
complete: () => {
|
|
1162
|
-
|
|
1163
|
-
for (const observer of
|
|
2269
|
+
endpoint.completed = true;
|
|
2270
|
+
for (const observer of endpoint.observers.values()) {
|
|
1164
2271
|
observer.complete?.();
|
|
1165
2272
|
}
|
|
1166
2273
|
}
|
|
1167
2274
|
});
|
|
1168
|
-
|
|
2275
|
+
endpoint.unsubscribe = () => subscription.unsubscribe();
|
|
1169
2276
|
}
|
|
1170
2277
|
} else {
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
2278
|
+
try {
|
|
2279
|
+
const data = await this.scheduleBatchedQuery(key, path, input, endpoint.mergedSelection);
|
|
2280
|
+
this.distributeData(endpoint, data);
|
|
2281
|
+
endpoint.completed = true;
|
|
2282
|
+
for (const observer of endpoint.observers.values()) {
|
|
1174
2283
|
observer.complete?.();
|
|
1175
2284
|
}
|
|
1176
|
-
})
|
|
2285
|
+
} catch (error) {
|
|
2286
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
2287
|
+
this.distributeError(endpoint, err);
|
|
2288
|
+
}
|
|
1177
2289
|
}
|
|
1178
2290
|
}
|
|
1179
2291
|
async executeMutation(path, input, select) {
|
|
@@ -1186,10 +2298,13 @@ class ClientImpl {
|
|
|
1186
2298
|
meta: select ? { select } : {}
|
|
1187
2299
|
};
|
|
1188
2300
|
const response = await this.execute(op2);
|
|
1189
|
-
if (response
|
|
1190
|
-
throw response.error;
|
|
2301
|
+
if (isError(response)) {
|
|
2302
|
+
throw new Error(response.error);
|
|
1191
2303
|
}
|
|
1192
|
-
|
|
2304
|
+
if (isSnapshot(response)) {
|
|
2305
|
+
return { data: response.data };
|
|
2306
|
+
}
|
|
2307
|
+
return { data: null };
|
|
1193
2308
|
}
|
|
1194
2309
|
createAccessor(path) {
|
|
1195
2310
|
return (descriptor) => {
|
|
@@ -1218,6 +2333,17 @@ class ClientImpl {
|
|
|
1218
2333
|
return queryResult;
|
|
1219
2334
|
};
|
|
1220
2335
|
}
|
|
2336
|
+
getStats() {
|
|
2337
|
+
let totalObservers = 0;
|
|
2338
|
+
for (const endpoint of this.endpoints.values()) {
|
|
2339
|
+
totalObservers += endpoint.observers.size;
|
|
2340
|
+
}
|
|
2341
|
+
return {
|
|
2342
|
+
endpointCount: this.endpoints.size,
|
|
2343
|
+
totalObservers,
|
|
2344
|
+
pendingBatches: this.pendingBatches.size
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
1221
2347
|
}
|
|
1222
2348
|
function createClient(config) {
|
|
1223
2349
|
const impl = new ClientImpl(config);
|
|
@@ -1309,16 +2435,17 @@ async function executeRequest(baseUrl, op2, options) {
|
|
|
1309
2435
|
}
|
|
1310
2436
|
if (!response.ok) {
|
|
1311
2437
|
return {
|
|
1312
|
-
error
|
|
2438
|
+
$: "error",
|
|
2439
|
+
error: `HTTP ${response.status}: ${response.statusText}`
|
|
1313
2440
|
};
|
|
1314
2441
|
}
|
|
1315
2442
|
const result = await response.json();
|
|
1316
2443
|
return result;
|
|
1317
2444
|
} catch (error) {
|
|
1318
2445
|
if (error instanceof Error && error.name === "AbortError") {
|
|
1319
|
-
return { error:
|
|
2446
|
+
return { $: "error", error: "Request timeout" };
|
|
1320
2447
|
}
|
|
1321
|
-
return { error };
|
|
2448
|
+
return { $: "error", error: error instanceof Error ? error.message : String(error) };
|
|
1322
2449
|
}
|
|
1323
2450
|
}
|
|
1324
2451
|
function createPollingObservable(baseUrl, op2, options) {
|
|
@@ -1331,24 +2458,28 @@ function createPollingObservable(baseUrl, op2, options) {
|
|
|
1331
2458
|
if (!active)
|
|
1332
2459
|
return;
|
|
1333
2460
|
try {
|
|
1334
|
-
const
|
|
2461
|
+
const message = await executeRequest(baseUrl, op2, {
|
|
1335
2462
|
headers: options.headers,
|
|
1336
2463
|
fetch: options.fetch
|
|
1337
2464
|
});
|
|
1338
2465
|
if (!active)
|
|
1339
2466
|
return;
|
|
1340
|
-
if (
|
|
2467
|
+
if (isError(message)) {
|
|
1341
2468
|
retries++;
|
|
1342
2469
|
if (retries > options.maxRetries) {
|
|
1343
|
-
observer.error?.(
|
|
2470
|
+
observer.error?.(new Error(message.error));
|
|
1344
2471
|
return;
|
|
1345
2472
|
}
|
|
1346
2473
|
} else {
|
|
1347
2474
|
retries = 0;
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
2475
|
+
if (isSnapshot(message)) {
|
|
2476
|
+
const hasDataChange = JSON.stringify(message.data) !== JSON.stringify(lastValue);
|
|
2477
|
+
if (hasDataChange) {
|
|
2478
|
+
lastValue = message.data;
|
|
2479
|
+
observer.next?.(message);
|
|
2480
|
+
}
|
|
2481
|
+
} else if (isOps(message)) {
|
|
2482
|
+
observer.next?.(message);
|
|
1352
2483
|
}
|
|
1353
2484
|
}
|
|
1354
2485
|
if (active) {
|
|
@@ -1397,217 +2528,6 @@ http.server = function httpServer(options) {
|
|
|
1397
2528
|
}
|
|
1398
2529
|
};
|
|
1399
2530
|
};
|
|
1400
|
-
class SubscriptionRegistry {
|
|
1401
|
-
subscriptions = new Map;
|
|
1402
|
-
entityIndex = new Map;
|
|
1403
|
-
add(sub) {
|
|
1404
|
-
const tracked = {
|
|
1405
|
-
...sub,
|
|
1406
|
-
state: "pending",
|
|
1407
|
-
lastDataHash: sub.lastData ? hashEntityState(sub.lastData) : null,
|
|
1408
|
-
createdAt: Date.now(),
|
|
1409
|
-
lastUpdateAt: null
|
|
1410
|
-
};
|
|
1411
|
-
this.subscriptions.set(sub.id, tracked);
|
|
1412
|
-
const entityKey = `${sub.entity}:${sub.entityId}`;
|
|
1413
|
-
let ids = this.entityIndex.get(entityKey);
|
|
1414
|
-
if (!ids) {
|
|
1415
|
-
ids = new Set;
|
|
1416
|
-
this.entityIndex.set(entityKey, ids);
|
|
1417
|
-
}
|
|
1418
|
-
ids.add(sub.id);
|
|
1419
|
-
}
|
|
1420
|
-
get(id) {
|
|
1421
|
-
return this.subscriptions.get(id);
|
|
1422
|
-
}
|
|
1423
|
-
has(id) {
|
|
1424
|
-
return this.subscriptions.has(id);
|
|
1425
|
-
}
|
|
1426
|
-
remove(id) {
|
|
1427
|
-
const sub = this.subscriptions.get(id);
|
|
1428
|
-
if (!sub)
|
|
1429
|
-
return;
|
|
1430
|
-
this.subscriptions.delete(id);
|
|
1431
|
-
const entityKey = `${sub.entity}:${sub.entityId}`;
|
|
1432
|
-
const ids = this.entityIndex.get(entityKey);
|
|
1433
|
-
if (ids) {
|
|
1434
|
-
ids.delete(id);
|
|
1435
|
-
if (ids.size === 0) {
|
|
1436
|
-
this.entityIndex.delete(entityKey);
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
}
|
|
1440
|
-
getByEntity(entity3, entityId) {
|
|
1441
|
-
const entityKey = `${entity3}:${entityId}`;
|
|
1442
|
-
const ids = this.entityIndex.get(entityKey);
|
|
1443
|
-
if (!ids)
|
|
1444
|
-
return [];
|
|
1445
|
-
const result = [];
|
|
1446
|
-
for (const id of ids) {
|
|
1447
|
-
const sub = this.subscriptions.get(id);
|
|
1448
|
-
if (sub) {
|
|
1449
|
-
result.push(sub);
|
|
1450
|
-
}
|
|
1451
|
-
}
|
|
1452
|
-
return result;
|
|
1453
|
-
}
|
|
1454
|
-
updateVersion(id, version, data) {
|
|
1455
|
-
const sub = this.subscriptions.get(id);
|
|
1456
|
-
if (!sub)
|
|
1457
|
-
return;
|
|
1458
|
-
sub.version = version;
|
|
1459
|
-
sub.lastUpdateAt = Date.now();
|
|
1460
|
-
if (data !== undefined) {
|
|
1461
|
-
sub.lastData = data;
|
|
1462
|
-
sub.lastDataHash = hashEntityState(data);
|
|
1463
|
-
}
|
|
1464
|
-
if (sub.state === "pending" || sub.state === "reconnecting") {
|
|
1465
|
-
sub.state = "active";
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
updateData(id, data) {
|
|
1469
|
-
const sub = this.subscriptions.get(id);
|
|
1470
|
-
if (!sub)
|
|
1471
|
-
return;
|
|
1472
|
-
sub.lastData = data;
|
|
1473
|
-
sub.lastDataHash = hashEntityState(data);
|
|
1474
|
-
}
|
|
1475
|
-
getLastData(id) {
|
|
1476
|
-
return this.subscriptions.get(id)?.lastData ?? null;
|
|
1477
|
-
}
|
|
1478
|
-
getVersion(id) {
|
|
1479
|
-
return this.subscriptions.get(id)?.version ?? null;
|
|
1480
|
-
}
|
|
1481
|
-
markActive(id) {
|
|
1482
|
-
const sub = this.subscriptions.get(id);
|
|
1483
|
-
if (sub) {
|
|
1484
|
-
sub.state = "active";
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
markError(id) {
|
|
1488
|
-
const sub = this.subscriptions.get(id);
|
|
1489
|
-
if (sub) {
|
|
1490
|
-
sub.state = "error";
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
markAllReconnecting() {
|
|
1494
|
-
for (const sub of this.subscriptions.values()) {
|
|
1495
|
-
if (sub.state === "active") {
|
|
1496
|
-
sub.state = "reconnecting";
|
|
1497
|
-
}
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
getByState(state) {
|
|
1501
|
-
const result = [];
|
|
1502
|
-
for (const sub of this.subscriptions.values()) {
|
|
1503
|
-
if (sub.state === state) {
|
|
1504
|
-
result.push(sub);
|
|
1505
|
-
}
|
|
1506
|
-
}
|
|
1507
|
-
return result;
|
|
1508
|
-
}
|
|
1509
|
-
getAllForReconnect() {
|
|
1510
|
-
const result = [];
|
|
1511
|
-
for (const sub of this.subscriptions.values()) {
|
|
1512
|
-
if (sub.state === "reconnecting" || sub.state === "active") {
|
|
1513
|
-
const reconnectSub = {
|
|
1514
|
-
id: sub.id,
|
|
1515
|
-
entity: sub.entity,
|
|
1516
|
-
entityId: sub.entityId,
|
|
1517
|
-
fields: sub.fields,
|
|
1518
|
-
version: sub.version,
|
|
1519
|
-
input: sub.input
|
|
1520
|
-
};
|
|
1521
|
-
if (sub.lastDataHash) {
|
|
1522
|
-
reconnectSub.dataHash = sub.lastDataHash;
|
|
1523
|
-
}
|
|
1524
|
-
result.push(reconnectSub);
|
|
1525
|
-
}
|
|
1526
|
-
}
|
|
1527
|
-
return result;
|
|
1528
|
-
}
|
|
1529
|
-
processReconnectResult(id, version, data) {
|
|
1530
|
-
const sub = this.subscriptions.get(id);
|
|
1531
|
-
if (!sub)
|
|
1532
|
-
return;
|
|
1533
|
-
sub.version = version;
|
|
1534
|
-
sub.state = "active";
|
|
1535
|
-
sub.lastUpdateAt = Date.now();
|
|
1536
|
-
if (data !== undefined) {
|
|
1537
|
-
sub.lastData = data;
|
|
1538
|
-
sub.lastDataHash = hashEntityState(data);
|
|
1539
|
-
}
|
|
1540
|
-
}
|
|
1541
|
-
getObserver(id) {
|
|
1542
|
-
return this.subscriptions.get(id)?.observer;
|
|
1543
|
-
}
|
|
1544
|
-
updateObserver(id, observer) {
|
|
1545
|
-
const sub = this.subscriptions.get(id);
|
|
1546
|
-
if (sub) {
|
|
1547
|
-
sub.observer = observer;
|
|
1548
|
-
}
|
|
1549
|
-
}
|
|
1550
|
-
notifyNext(id, data) {
|
|
1551
|
-
const sub = this.subscriptions.get(id);
|
|
1552
|
-
sub?.observer.next?.({ data, version: sub.version });
|
|
1553
|
-
}
|
|
1554
|
-
notifyError(id, error) {
|
|
1555
|
-
this.subscriptions.get(id)?.observer.error?.(error);
|
|
1556
|
-
}
|
|
1557
|
-
notifyAllReconnectingError(error) {
|
|
1558
|
-
for (const sub of this.subscriptions.values()) {
|
|
1559
|
-
if (sub.state === "reconnecting") {
|
|
1560
|
-
sub.observer.error?.(error);
|
|
1561
|
-
}
|
|
1562
|
-
}
|
|
1563
|
-
}
|
|
1564
|
-
get size() {
|
|
1565
|
-
return this.subscriptions.size;
|
|
1566
|
-
}
|
|
1567
|
-
getIds() {
|
|
1568
|
-
return Array.from(this.subscriptions.keys());
|
|
1569
|
-
}
|
|
1570
|
-
values() {
|
|
1571
|
-
return this.subscriptions.values();
|
|
1572
|
-
}
|
|
1573
|
-
getStats() {
|
|
1574
|
-
const byState = {
|
|
1575
|
-
pending: 0,
|
|
1576
|
-
active: 0,
|
|
1577
|
-
reconnecting: 0,
|
|
1578
|
-
error: 0
|
|
1579
|
-
};
|
|
1580
|
-
const byEntity = {};
|
|
1581
|
-
for (const sub of this.subscriptions.values()) {
|
|
1582
|
-
byState[sub.state]++;
|
|
1583
|
-
const entityKey = `${sub.entity}:${sub.entityId}`;
|
|
1584
|
-
byEntity[entityKey] = (byEntity[entityKey] ?? 0) + 1;
|
|
1585
|
-
}
|
|
1586
|
-
return {
|
|
1587
|
-
total: this.subscriptions.size,
|
|
1588
|
-
byState,
|
|
1589
|
-
byEntity
|
|
1590
|
-
};
|
|
1591
|
-
}
|
|
1592
|
-
clear() {
|
|
1593
|
-
for (const sub of this.subscriptions.values()) {
|
|
1594
|
-
sub.observer.complete?.();
|
|
1595
|
-
}
|
|
1596
|
-
this.subscriptions.clear();
|
|
1597
|
-
this.entityIndex.clear();
|
|
1598
|
-
}
|
|
1599
|
-
clearErrors() {
|
|
1600
|
-
const toRemove = [];
|
|
1601
|
-
for (const [id, sub] of this.subscriptions) {
|
|
1602
|
-
if (sub.state === "error") {
|
|
1603
|
-
toRemove.push(id);
|
|
1604
|
-
}
|
|
1605
|
-
}
|
|
1606
|
-
for (const id of toRemove) {
|
|
1607
|
-
this.remove(id);
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
2531
|
|
|
1612
2532
|
// src/create.ts
|
|
1613
2533
|
import { createEffect, createSignal, on, onCleanup } from "solid-js";
|