js-bao 0.5.2 → 0.6.1
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 +4 -0
- package/dist/browser.cjs +4069 -350
- package/dist/browser.d.cts +2269 -50
- package/dist/browser.d.ts +2269 -50
- package/dist/browser.js +4070 -351
- package/dist/client.cjs +343 -2
- package/dist/client.d.cts +739 -12
- package/dist/client.d.ts +739 -12
- package/dist/client.js +340 -1
- package/dist/cloudflare-do.cjs +847 -6
- package/dist/cloudflare-do.d.cts +1085 -13
- package/dist/cloudflare-do.d.ts +1085 -13
- package/dist/cloudflare-do.js +845 -5
- package/dist/cloudflare.cjs +359 -10
- package/dist/cloudflare.d.cts +751 -13
- package/dist/cloudflare.d.ts +751 -13
- package/dist/cloudflare.js +356 -9
- package/dist/codegen-v2.cjs +319 -3
- package/dist/codegen.cjs +1 -1
- package/dist/index.cjs +4325 -501
- package/dist/index.d.cts +2253 -18
- package/dist/index.d.ts +2253 -18
- package/dist/index.js +4238 -470
- package/dist/node.cjs +4167 -359
- package/dist/node.d.cts +2280 -58
- package/dist/node.d.ts +2280 -58
- package/dist/node.js +4117 -361
- package/package.json +1 -1
package/dist/cloudflare-do.cjs
CHANGED
|
@@ -379,6 +379,9 @@ var init_patterns = __esm({
|
|
|
379
379
|
});
|
|
380
380
|
|
|
381
381
|
// src/utils/yjsValues.ts
|
|
382
|
+
function isYjsComposite(value) {
|
|
383
|
+
return value instanceof Y.Map || value instanceof Y.Array || value instanceof Y.Text;
|
|
384
|
+
}
|
|
382
385
|
function isStringSetYMap(value) {
|
|
383
386
|
if (!(value instanceof Y.Map)) return false;
|
|
384
387
|
for (const v of value.values()) {
|
|
@@ -599,15 +602,525 @@ var init_uniqueIndex = __esm({
|
|
|
599
602
|
}
|
|
600
603
|
});
|
|
601
604
|
|
|
605
|
+
// src/utils/largeDocuments.ts
|
|
606
|
+
function encodeOverlaySegment(segment) {
|
|
607
|
+
const escaped = segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
608
|
+
return escaped.startsWith("_") ? `~2${escaped.slice(1)}` : escaped;
|
|
609
|
+
}
|
|
610
|
+
function decodeOverlaySegment(segment) {
|
|
611
|
+
const withUnderscore = segment.startsWith("~2") ? `_${segment.slice(2)}` : segment;
|
|
612
|
+
return withUnderscore.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
613
|
+
}
|
|
614
|
+
function overlayFieldKey(recordId, field) {
|
|
615
|
+
return `${encodeOverlaySegment(recordId)}${OVERLAY_KEY_SEPARATOR}${encodeOverlaySegment(field)}`;
|
|
616
|
+
}
|
|
617
|
+
function overlayMemberKey(recordId, field, member) {
|
|
618
|
+
return `${overlayFieldKey(recordId, field)}${OVERLAY_KEY_SEPARATOR}${encodeOverlaySegment(member)}`;
|
|
619
|
+
}
|
|
620
|
+
function overlayMarkerKey(recordId, marker) {
|
|
621
|
+
return `${encodeOverlaySegment(recordId)}${OVERLAY_KEY_SEPARATOR}${marker}`;
|
|
622
|
+
}
|
|
623
|
+
function parseOverlayKey(key) {
|
|
624
|
+
const parts = key.split(OVERLAY_KEY_SEPARATOR);
|
|
625
|
+
if (parts.length === 2) {
|
|
626
|
+
const [rawId, rawSecond] = parts;
|
|
627
|
+
const recordId = decodeOverlaySegment(rawId);
|
|
628
|
+
if (OVERLAY_MARKERS.has(rawSecond)) return { recordId, marker: rawSecond };
|
|
629
|
+
return { recordId, field: decodeOverlaySegment(rawSecond) };
|
|
630
|
+
}
|
|
631
|
+
if (parts.length === 3) {
|
|
632
|
+
return {
|
|
633
|
+
recordId: decodeOverlaySegment(parts[0]),
|
|
634
|
+
field: decodeOverlaySegment(parts[1]),
|
|
635
|
+
member: decodeOverlaySegment(parts[2])
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
function emptyEntry(id) {
|
|
641
|
+
return { id, fields: {}, stringSets: {}, replace: false, deleted: false };
|
|
642
|
+
}
|
|
643
|
+
function groupOverlayEntries(entries) {
|
|
644
|
+
const out = /* @__PURE__ */ new Map();
|
|
645
|
+
for (const [key, value] of entries) {
|
|
646
|
+
const parsed = parseOverlayKey(key);
|
|
647
|
+
if (!parsed) continue;
|
|
648
|
+
let entry = out.get(parsed.recordId);
|
|
649
|
+
if (!entry) {
|
|
650
|
+
entry = emptyEntry(parsed.recordId);
|
|
651
|
+
out.set(parsed.recordId, entry);
|
|
652
|
+
}
|
|
653
|
+
if (parsed.marker === OVERLAY_MARKER_REPLACE) {
|
|
654
|
+
entry.replace = value === true;
|
|
655
|
+
} else if (parsed.marker === OVERLAY_MARKER_DELETED) {
|
|
656
|
+
entry.deleted = value === true;
|
|
657
|
+
} else if (parsed.member !== void 0) {
|
|
658
|
+
const field = parsed.field;
|
|
659
|
+
(entry.stringSets[field] ||= {})[parsed.member] = value === true;
|
|
660
|
+
} else if (parsed.field !== void 0) {
|
|
661
|
+
entry.fields[parsed.field] = value;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
return out;
|
|
665
|
+
}
|
|
666
|
+
function readOverlayRecord(modelMap, recordId) {
|
|
667
|
+
return collectOverlayRecords(modelMap, /* @__PURE__ */ new Set([recordId])).get(recordId);
|
|
668
|
+
}
|
|
669
|
+
function completeOverlayEntries(modelMap, entries) {
|
|
670
|
+
const rebuild = /* @__PURE__ */ new Set();
|
|
671
|
+
for (const [id, entry] of entries) {
|
|
672
|
+
if (!entry.replace || entry.deleted) continue;
|
|
673
|
+
rebuild.add(id);
|
|
674
|
+
}
|
|
675
|
+
if (rebuild.size === 0) return entries;
|
|
676
|
+
for (const [id, entry] of collectOverlayRecords(modelMap, rebuild)) {
|
|
677
|
+
entries.set(id, entry);
|
|
678
|
+
}
|
|
679
|
+
return entries;
|
|
680
|
+
}
|
|
681
|
+
function collectOverlayRecords(modelMap, recordIds) {
|
|
682
|
+
const wanted = /* @__PURE__ */ new Map();
|
|
683
|
+
for (const id of recordIds) wanted.set(encodeOverlaySegment(id), id);
|
|
684
|
+
const own = /* @__PURE__ */ new Map();
|
|
685
|
+
for (const [key, value] of modelMap.entries()) {
|
|
686
|
+
if (typeof key !== "string") continue;
|
|
687
|
+
const separator = key.indexOf(OVERLAY_KEY_SEPARATOR);
|
|
688
|
+
if (separator <= 0) continue;
|
|
689
|
+
const id = wanted.get(key.slice(0, separator));
|
|
690
|
+
if (id === void 0) continue;
|
|
691
|
+
if (!parseOverlayKey(key)) continue;
|
|
692
|
+
const keys = own.get(id);
|
|
693
|
+
if (keys) keys.push([key, value]);
|
|
694
|
+
else own.set(id, [[key, value]]);
|
|
695
|
+
}
|
|
696
|
+
const out = /* @__PURE__ */ new Map();
|
|
697
|
+
for (const id of recordIds) {
|
|
698
|
+
out.set(id, groupOverlayEntries(own.get(id) ?? []).get(id) ?? emptyEntry(id));
|
|
699
|
+
}
|
|
700
|
+
return out;
|
|
701
|
+
}
|
|
702
|
+
function baseMembers(value) {
|
|
703
|
+
if (Array.isArray(value)) return value.filter((v) => typeof v === "string");
|
|
704
|
+
if (value && typeof value === "object") {
|
|
705
|
+
return Object.entries(value).filter(([, present]) => Boolean(present)).map(([member]) => member);
|
|
706
|
+
}
|
|
707
|
+
return [];
|
|
708
|
+
}
|
|
709
|
+
function materializeOverlayRecord(base, overlay) {
|
|
710
|
+
if (overlay.deleted) return null;
|
|
711
|
+
const out = overlay.replace ? {} : { ...base ?? {} };
|
|
712
|
+
for (const [field, value] of Object.entries(overlay.fields)) {
|
|
713
|
+
if (value === null) delete out[field];
|
|
714
|
+
else out[field] = value;
|
|
715
|
+
}
|
|
716
|
+
for (const [field, members] of Object.entries(overlay.stringSets)) {
|
|
717
|
+
const current = overlay.replace ? [] : baseMembers(out[field]);
|
|
718
|
+
const next = current.slice();
|
|
719
|
+
for (const [member, present] of Object.entries(members)) {
|
|
720
|
+
const at = next.indexOf(member);
|
|
721
|
+
if (present && at === -1) next.push(member);
|
|
722
|
+
else if (!present && at !== -1) next.splice(at, 1);
|
|
723
|
+
}
|
|
724
|
+
out[field] = next;
|
|
725
|
+
}
|
|
726
|
+
out.id = overlay.id;
|
|
727
|
+
return out;
|
|
728
|
+
}
|
|
729
|
+
function encodeOverlayMutation(mutation) {
|
|
730
|
+
const { id, kind } = mutation;
|
|
731
|
+
const entries = [];
|
|
732
|
+
if (kind === "delete") {
|
|
733
|
+
entries.push([overlayMarkerKey(id, OVERLAY_MARKER_DELETED), true]);
|
|
734
|
+
return entries;
|
|
735
|
+
}
|
|
736
|
+
if (kind === "create") {
|
|
737
|
+
entries.push([overlayMarkerKey(id, OVERLAY_MARKER_REPLACE), true]);
|
|
738
|
+
entries.push([overlayMarkerKey(id, OVERLAY_MARKER_DELETED), false]);
|
|
739
|
+
}
|
|
740
|
+
for (const [field, value] of Object.entries(mutation.fields ?? {})) {
|
|
741
|
+
entries.push([overlayFieldKey(id, field), value === void 0 ? null : value]);
|
|
742
|
+
}
|
|
743
|
+
for (const [field, members] of Object.entries(mutation.stringSetDeltas ?? {})) {
|
|
744
|
+
for (const [member, present] of Object.entries(members)) {
|
|
745
|
+
entries.push([overlayMemberKey(id, field, member), present]);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return entries;
|
|
749
|
+
}
|
|
750
|
+
function applyOverlayMutation(modelMap, mutation) {
|
|
751
|
+
if (mutation.kind === "create") {
|
|
752
|
+
clearStaleRecordOverlay(modelMap, mutation.id);
|
|
753
|
+
}
|
|
754
|
+
for (const [key, value] of encodeOverlayMutation(mutation)) {
|
|
755
|
+
modelMap.set(key, value);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
function clearStaleRecordOverlay(modelMap, recordId) {
|
|
759
|
+
if (!modelMap.has(overlayMarkerKey(recordId, OVERLAY_MARKER_DELETED))) return;
|
|
760
|
+
const prefix = `${encodeOverlaySegment(recordId)}${OVERLAY_KEY_SEPARATOR}`;
|
|
761
|
+
const stale = [];
|
|
762
|
+
for (const key of modelMap.keys()) {
|
|
763
|
+
if (key.startsWith(prefix)) stale.push(key);
|
|
764
|
+
}
|
|
765
|
+
for (const key of stale) modelMap.delete(key);
|
|
766
|
+
}
|
|
767
|
+
function assertNoFormat2CompositeFields(modelName, recordId, fields) {
|
|
768
|
+
if (!fields) return;
|
|
769
|
+
for (const [field, value] of Object.entries(fields)) {
|
|
770
|
+
if (isYjsComposite(value)) {
|
|
771
|
+
throw new Format2CompositeFieldError(modelName, recordId, field);
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
function isFormat2StringSetYMap(value) {
|
|
776
|
+
if (!(value instanceof Y6.Map)) return false;
|
|
777
|
+
for (const member of value.values()) {
|
|
778
|
+
if (member !== true && member !== false) return false;
|
|
779
|
+
}
|
|
780
|
+
return true;
|
|
781
|
+
}
|
|
782
|
+
function format2StringSetMembers(value) {
|
|
783
|
+
if (!(value instanceof Y6.Map)) return [];
|
|
784
|
+
return Array.from(value.entries()).filter(([, present]) => present === true).map(([member]) => member);
|
|
785
|
+
}
|
|
786
|
+
var Y6, OVERLAY_KEY_SEPARATOR, OVERLAY_MARKER_REPLACE, OVERLAY_MARKER_DELETED, OVERLAY_MARKERS, Format2CompositeFieldError;
|
|
787
|
+
var init_largeDocuments = __esm({
|
|
788
|
+
"src/utils/largeDocuments.ts"() {
|
|
789
|
+
"use strict";
|
|
790
|
+
Y6 = __toESM(require("yjs"), 1);
|
|
791
|
+
init_yjsValues();
|
|
792
|
+
OVERLAY_KEY_SEPARATOR = "/";
|
|
793
|
+
OVERLAY_MARKER_REPLACE = "_replace";
|
|
794
|
+
OVERLAY_MARKER_DELETED = "_deleted";
|
|
795
|
+
OVERLAY_MARKERS = /* @__PURE__ */ new Set([
|
|
796
|
+
OVERLAY_MARKER_REPLACE,
|
|
797
|
+
OVERLAY_MARKER_DELETED
|
|
798
|
+
]);
|
|
799
|
+
Format2CompositeFieldError = class extends Error {
|
|
800
|
+
modelName;
|
|
801
|
+
recordId;
|
|
802
|
+
field;
|
|
803
|
+
constructor(modelName, recordId, field) {
|
|
804
|
+
super(
|
|
805
|
+
`Composite field values are not supported on large documents (format 2): ${modelName}.${field} (id=${recordId})`
|
|
806
|
+
);
|
|
807
|
+
this.name = "Format2CompositeFieldError";
|
|
808
|
+
this.modelName = modelName;
|
|
809
|
+
this.recordId = recordId;
|
|
810
|
+
this.field = field;
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
// src/models/OrmContext.ts
|
|
817
|
+
var OrmContext, defaultOrmContext, liveOrmContexts;
|
|
818
|
+
var init_OrmContext = __esm({
|
|
819
|
+
"src/models/OrmContext.ts"() {
|
|
820
|
+
"use strict";
|
|
821
|
+
OrmContext = class _OrmContext {
|
|
822
|
+
/** Declared before the field that reads it: static initialization is ordered. */
|
|
823
|
+
static instanceCounter = 0;
|
|
824
|
+
/**
|
|
825
|
+
* Which instance this is, for diagnostics. A process holding several clients
|
|
826
|
+
* needs a way to say WHICH one a warning is about, so every context takes the
|
|
827
|
+
* next number as it is created and keeps it for its lifetime.
|
|
828
|
+
*/
|
|
829
|
+
instanceId = ++_OrmContext.instanceCounter;
|
|
830
|
+
/** The engine this instance's models read and write through. */
|
|
831
|
+
dbEngine = null;
|
|
832
|
+
connectedDocuments = /* @__PURE__ */ new Map();
|
|
833
|
+
/** `docId_modelName` → the Y.Map that document holds for that model. */
|
|
834
|
+
documentYMaps = /* @__PURE__ */ new Map();
|
|
835
|
+
/** `subscribe()` callbacks, by model name. */
|
|
836
|
+
listenersMap = /* @__PURE__ */ new Map();
|
|
837
|
+
// Default-document resolution
|
|
838
|
+
modelNameToDefaultDocId = /* @__PURE__ */ new Map();
|
|
839
|
+
globalDefaultDocId = void 0;
|
|
840
|
+
defaultDocChangedListeners = /* @__PURE__ */ new Set();
|
|
841
|
+
modelDocMappingChangedListeners = /* @__PURE__ */ new Set();
|
|
842
|
+
/** Format-2 ("large document", #2816) record stores by document id. */
|
|
843
|
+
format2Documents = /* @__PURE__ */ new Map();
|
|
844
|
+
/** Format-2 epoch moves in flight (#2816). */
|
|
845
|
+
format2Transitions = /* @__PURE__ */ new Map();
|
|
846
|
+
/**
|
|
847
|
+
* Nested-Y.Map stringset fields this context has already observed. Per
|
|
848
|
+
* context, not global: a Y.Doc reconnected to a fresh instance has to be
|
|
849
|
+
* observed again by that instance, and a global guard would silently
|
|
850
|
+
* suppress it.
|
|
851
|
+
*/
|
|
852
|
+
observedStringSetMaps = /* @__PURE__ */ new WeakSet();
|
|
853
|
+
/**
|
|
854
|
+
* The registry this instance's schema lookups use. `null` on the standing
|
|
855
|
+
* default context, where callers fall back to the global singleton.
|
|
856
|
+
*/
|
|
857
|
+
modelRegistry = null;
|
|
858
|
+
/** Model classes routed to this context. */
|
|
859
|
+
boundModels = /* @__PURE__ */ new Set();
|
|
860
|
+
/** True once {@link destroy} has run; bound classes keep pointing here. */
|
|
861
|
+
destroyed = false;
|
|
862
|
+
/** Yjs `unobserve` thunks by owning document id. */
|
|
863
|
+
observerDisposers = /* @__PURE__ */ new Map();
|
|
864
|
+
/** Teardown the owner (the document manager) registers for `destroy()`. */
|
|
865
|
+
teardownHooks = /* @__PURE__ */ new Set();
|
|
866
|
+
static NO_DOCUMENT_KEY = "__no_document__";
|
|
867
|
+
/**
|
|
868
|
+
* Record how to remove an observer this context installed. Every `observe()`
|
|
869
|
+
* in the ORM pairs with one of these: without it a torn-down instance's
|
|
870
|
+
* callbacks keep firing on a Y.Doc that outlives it.
|
|
871
|
+
*/
|
|
872
|
+
registerObserverDisposer(docId, dispose) {
|
|
873
|
+
const key = docId ?? _OrmContext.NO_DOCUMENT_KEY;
|
|
874
|
+
const list = this.observerDisposers.get(key);
|
|
875
|
+
if (list) list.push(dispose);
|
|
876
|
+
else this.observerDisposers.set(key, [dispose]);
|
|
877
|
+
}
|
|
878
|
+
/** Remove every observer this context installed for `docId`. */
|
|
879
|
+
disposeObserversForDocument(docId) {
|
|
880
|
+
const list = this.observerDisposers.get(docId);
|
|
881
|
+
if (!list) return;
|
|
882
|
+
this.observerDisposers.delete(docId);
|
|
883
|
+
for (const dispose of list) {
|
|
884
|
+
try {
|
|
885
|
+
dispose();
|
|
886
|
+
} catch {
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
disposeAllObservers() {
|
|
891
|
+
const buckets = Array.from(this.observerDisposers.values());
|
|
892
|
+
this.observerDisposers.clear();
|
|
893
|
+
for (const list of buckets) {
|
|
894
|
+
for (const dispose of list) {
|
|
895
|
+
try {
|
|
896
|
+
dispose();
|
|
897
|
+
} catch {
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
/** Run `hook` as part of `destroy()` (the format-2 binding registers here). */
|
|
903
|
+
onDestroy(hook) {
|
|
904
|
+
this.teardownHooks.add(hook);
|
|
905
|
+
}
|
|
906
|
+
/** The record store for `docId`, or `null` when the document is format 1. */
|
|
907
|
+
getFormat2Store(docId) {
|
|
908
|
+
if (!docId) return null;
|
|
909
|
+
return this.format2Documents.get(docId) ?? null;
|
|
910
|
+
}
|
|
911
|
+
/**
|
|
912
|
+
* The epoch move a write to `docId` has to wait out (#2816, behavior 10), or
|
|
913
|
+
* `null` when none is in flight.
|
|
914
|
+
*/
|
|
915
|
+
format2WritesHeld(docId) {
|
|
916
|
+
if (!docId) return null;
|
|
917
|
+
return this.format2Transitions.get(docId) ?? null;
|
|
918
|
+
}
|
|
919
|
+
/** Run an epoch move with `docId`'s local writes held back (#2816). */
|
|
920
|
+
async holdFormat2Writes(docId, run) {
|
|
921
|
+
const inFlight = this.format2Transitions.get(docId);
|
|
922
|
+
if (inFlight) await inFlight.catch(() => {
|
|
923
|
+
});
|
|
924
|
+
let release;
|
|
925
|
+
const held = new Promise((resolve) => {
|
|
926
|
+
release = resolve;
|
|
927
|
+
});
|
|
928
|
+
this.format2Transitions.set(docId, held);
|
|
929
|
+
try {
|
|
930
|
+
return await run();
|
|
931
|
+
} finally {
|
|
932
|
+
if (this.format2Transitions.get(docId) === held) {
|
|
933
|
+
this.format2Transitions.delete(docId);
|
|
934
|
+
}
|
|
935
|
+
release();
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
/** Notify `subscribe()` callbacks registered on this context for `modelName`. */
|
|
939
|
+
notifyListeners(modelName, log, onError) {
|
|
940
|
+
const listeners = this.listenersMap.get(modelName);
|
|
941
|
+
if (!listeners) return;
|
|
942
|
+
log?.(`Notifying ${listeners.size} listeners for ${modelName}`);
|
|
943
|
+
listeners.forEach((callback) => {
|
|
944
|
+
try {
|
|
945
|
+
callback();
|
|
946
|
+
} catch (e) {
|
|
947
|
+
onError?.(e);
|
|
948
|
+
}
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
// ---- Default-document resolution (the `BaseModel` statics delegate here) ----
|
|
952
|
+
setModelDefaultDocumentId(modelName, docId) {
|
|
953
|
+
const previous = this.modelNameToDefaultDocId.get(modelName);
|
|
954
|
+
this.modelNameToDefaultDocId.set(modelName, docId);
|
|
955
|
+
this.announceModelDocMapping(modelName, previous, docId);
|
|
956
|
+
}
|
|
957
|
+
removeModelDefaultDocumentId(modelName) {
|
|
958
|
+
const previous = this.modelNameToDefaultDocId.get(modelName);
|
|
959
|
+
this.modelNameToDefaultDocId.delete(modelName);
|
|
960
|
+
this.announceModelDocMapping(modelName, previous, void 0);
|
|
961
|
+
}
|
|
962
|
+
clearModelDefaultDocumentIds() {
|
|
963
|
+
const entries = Array.from(this.modelNameToDefaultDocId.entries());
|
|
964
|
+
this.modelNameToDefaultDocId.clear();
|
|
965
|
+
for (const [modelName, previous] of entries) {
|
|
966
|
+
this.announceModelDocMapping(modelName, previous, void 0);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
setGlobalDefaultDocumentId(docId) {
|
|
970
|
+
const previous = this.globalDefaultDocId;
|
|
971
|
+
this.globalDefaultDocId = docId;
|
|
972
|
+
this.announceDefaultDoc(previous, docId);
|
|
973
|
+
}
|
|
974
|
+
clearGlobalDefaultDocumentId() {
|
|
975
|
+
const previous = this.globalDefaultDocId;
|
|
976
|
+
this.globalDefaultDocId = void 0;
|
|
977
|
+
this.announceDefaultDoc(previous, void 0);
|
|
978
|
+
}
|
|
979
|
+
getModelDefaultDocumentMapping() {
|
|
980
|
+
return Object.fromEntries(this.modelNameToDefaultDocId.entries());
|
|
981
|
+
}
|
|
982
|
+
getDocumentIdForModel(modelName) {
|
|
983
|
+
return this.modelNameToDefaultDocId.get(modelName);
|
|
984
|
+
}
|
|
985
|
+
getGlobalDefaultDocumentId() {
|
|
986
|
+
return this.globalDefaultDocId;
|
|
987
|
+
}
|
|
988
|
+
onDefaultDocChanged(listener) {
|
|
989
|
+
this.defaultDocChangedListeners.add(listener);
|
|
990
|
+
return () => this.defaultDocChangedListeners.delete(listener);
|
|
991
|
+
}
|
|
992
|
+
onModelDocMappingChanged(listener) {
|
|
993
|
+
this.modelDocMappingChangedListeners.add(listener);
|
|
994
|
+
return () => this.modelDocMappingChangedListeners.delete(listener);
|
|
995
|
+
}
|
|
996
|
+
/** Drop the model→document defaults that point at a disconnected document. */
|
|
997
|
+
clearMappingsForDocId(docId) {
|
|
998
|
+
const toRemove = [];
|
|
999
|
+
for (const [modelName, mappedDocId] of this.modelNameToDefaultDocId) {
|
|
1000
|
+
if (mappedDocId === docId) toRemove.push(modelName);
|
|
1001
|
+
}
|
|
1002
|
+
for (const modelName of toRemove) {
|
|
1003
|
+
this.removeModelDefaultDocumentId(modelName);
|
|
1004
|
+
}
|
|
1005
|
+
if (this.globalDefaultDocId === docId) {
|
|
1006
|
+
this.clearGlobalDefaultDocumentId();
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
announceDefaultDoc(previous, current) {
|
|
1010
|
+
for (const listener of this.defaultDocChangedListeners) {
|
|
1011
|
+
try {
|
|
1012
|
+
listener({ previous, current });
|
|
1013
|
+
} catch {
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
announceModelDocMapping(modelName, previous, current) {
|
|
1018
|
+
for (const listener of this.modelDocMappingChangedListeners) {
|
|
1019
|
+
try {
|
|
1020
|
+
listener({ modelName, previous, current });
|
|
1021
|
+
} catch {
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Tear this instance down: observers off, engine destroyed, state cleared.
|
|
1027
|
+
*
|
|
1028
|
+
* Deliberately does NOT unbind its model classes. They keep resolving this
|
|
1029
|
+
* context, whose engine is now null, so an operation through them throws the
|
|
1030
|
+
* ordinary not-initialized error instead of silently landing on whatever
|
|
1031
|
+
* other instance happens to be live (#3039, behavior 7). Only a later
|
|
1032
|
+
* `initJsBao` may rebind a class whose context is destroyed.
|
|
1033
|
+
*/
|
|
1034
|
+
async destroy() {
|
|
1035
|
+
if (this.destroyed) return;
|
|
1036
|
+
this.destroyed = true;
|
|
1037
|
+
this.disposeAllObservers();
|
|
1038
|
+
const hooks = Array.from(this.teardownHooks);
|
|
1039
|
+
this.teardownHooks.clear();
|
|
1040
|
+
for (const hook of hooks) {
|
|
1041
|
+
try {
|
|
1042
|
+
await hook();
|
|
1043
|
+
} catch {
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
const engine = this.dbEngine;
|
|
1047
|
+
this.dbEngine = null;
|
|
1048
|
+
if (engine) {
|
|
1049
|
+
try {
|
|
1050
|
+
await engine.destroy();
|
|
1051
|
+
} catch {
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
this.connectedDocuments.clear();
|
|
1055
|
+
this.documentYMaps.clear();
|
|
1056
|
+
this.listenersMap.clear();
|
|
1057
|
+
this.modelNameToDefaultDocId.clear();
|
|
1058
|
+
this.globalDefaultDocId = void 0;
|
|
1059
|
+
this.defaultDocChangedListeners.clear();
|
|
1060
|
+
this.modelDocMappingChangedListeners.clear();
|
|
1061
|
+
this.format2Documents.clear();
|
|
1062
|
+
this.format2Transitions.clear();
|
|
1063
|
+
this.observedStringSetMaps = /* @__PURE__ */ new WeakSet();
|
|
1064
|
+
liveOrmContexts.delete(this);
|
|
1065
|
+
if (defaultOrmContext === this) {
|
|
1066
|
+
defaultOrmContext = new _OrmContext();
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
};
|
|
1070
|
+
defaultOrmContext = new OrmContext();
|
|
1071
|
+
liveOrmContexts = /* @__PURE__ */ new Set();
|
|
1072
|
+
}
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
// src/models/format2QueryProjection.ts
|
|
1076
|
+
var FORMAT2_QUERY_PROJECTION_DDL;
|
|
1077
|
+
var init_format2QueryProjection = __esm({
|
|
1078
|
+
"src/models/format2QueryProjection.ts"() {
|
|
1079
|
+
"use strict";
|
|
1080
|
+
FORMAT2_QUERY_PROJECTION_DDL = `
|
|
1081
|
+
CREATE TABLE IF NOT EXISTS _query_projection (
|
|
1082
|
+
doc_id TEXT NOT NULL,
|
|
1083
|
+
model TEXT NOT NULL,
|
|
1084
|
+
PRIMARY KEY (doc_id, model)
|
|
1085
|
+
)`.trim();
|
|
1086
|
+
}
|
|
1087
|
+
});
|
|
1088
|
+
|
|
1089
|
+
// src/models/format2Observer.ts
|
|
1090
|
+
var Y7;
|
|
1091
|
+
var init_format2Observer = __esm({
|
|
1092
|
+
"src/models/format2Observer.ts"() {
|
|
1093
|
+
"use strict";
|
|
1094
|
+
Y7 = __toESM(require("yjs"), 1);
|
|
1095
|
+
init_largeDocuments();
|
|
1096
|
+
}
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
// src/models/format2ClientWrite.ts
|
|
1100
|
+
var init_format2ClientWrite = __esm({
|
|
1101
|
+
"src/models/format2ClientWrite.ts"() {
|
|
1102
|
+
"use strict";
|
|
1103
|
+
}
|
|
1104
|
+
});
|
|
1105
|
+
|
|
1106
|
+
// src/models/format2OfflineWindow.ts
|
|
1107
|
+
var DAY_MS;
|
|
1108
|
+
var init_format2OfflineWindow = __esm({
|
|
1109
|
+
"src/models/format2OfflineWindow.ts"() {
|
|
1110
|
+
"use strict";
|
|
1111
|
+
DAY_MS = 24 * 60 * 60 * 1e3;
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
|
|
602
1115
|
// src/models/BaseModel.ts
|
|
603
1116
|
function generateULID() {
|
|
604
1117
|
return (0, import_ulid3.ulid)();
|
|
605
1118
|
}
|
|
606
|
-
var
|
|
1119
|
+
var Y8, import_ulid3;
|
|
607
1120
|
var init_BaseModel = __esm({
|
|
608
1121
|
"src/models/BaseModel.ts"() {
|
|
609
1122
|
"use strict";
|
|
610
|
-
|
|
1123
|
+
Y8 = __toESM(require("yjs"), 1);
|
|
611
1124
|
import_ulid3 = require("ulid");
|
|
612
1125
|
init_StringSet();
|
|
613
1126
|
init_documentTypes();
|
|
@@ -618,6 +1131,12 @@ var init_BaseModel = __esm({
|
|
|
618
1131
|
init_writeLifecycle();
|
|
619
1132
|
init_yjsValues();
|
|
620
1133
|
init_uniqueIndex();
|
|
1134
|
+
init_largeDocuments();
|
|
1135
|
+
init_OrmContext();
|
|
1136
|
+
init_format2QueryProjection();
|
|
1137
|
+
init_format2Observer();
|
|
1138
|
+
init_format2ClientWrite();
|
|
1139
|
+
init_format2OfflineWindow();
|
|
621
1140
|
registerFunctionDefault(generateULID, "generate_ulid");
|
|
622
1141
|
}
|
|
623
1142
|
});
|
|
@@ -636,32 +1155,63 @@ var init_ModelRegistry = __esm({
|
|
|
636
1155
|
"use strict";
|
|
637
1156
|
init_BaseModel();
|
|
638
1157
|
init_relationshipManager();
|
|
1158
|
+
init_format2QueryProjection();
|
|
639
1159
|
}
|
|
640
1160
|
});
|
|
641
1161
|
|
|
642
1162
|
// src/cloudflare-do.ts
|
|
643
1163
|
var cloudflare_do_exports = {};
|
|
644
1164
|
__export(cloudflare_do_exports, {
|
|
1165
|
+
DEFAULT_OVERLAY_TABLES: () => DEFAULT_OVERLAY_TABLES,
|
|
645
1166
|
DurableObjectEngine: () => DurableObjectEngine,
|
|
1167
|
+
Format2CompositeFieldError: () => Format2CompositeFieldError,
|
|
646
1168
|
JsonQueryTranslator: () => JsonQueryTranslator,
|
|
647
1169
|
JsonSchemaDDL: () => JsonSchemaDDL,
|
|
1170
|
+
OVERLAY_KEY_SEPARATOR: () => OVERLAY_KEY_SEPARATOR,
|
|
1171
|
+
OVERLAY_MARKER_DELETED: () => OVERLAY_MARKER_DELETED,
|
|
1172
|
+
OVERLAY_MARKER_REPLACE: () => OVERLAY_MARKER_REPLACE,
|
|
648
1173
|
PROJECTED_JSON_ALIAS_PREFIX: () => PROJECTED_JSON_ALIAS_PREFIX,
|
|
1174
|
+
SnapshotChunkIntegrityError: () => SnapshotChunkIntegrityError,
|
|
1175
|
+
SnapshotRowFormatError: () => SnapshotRowFormatError,
|
|
1176
|
+
applyOverlayMutation: () => applyOverlayMutation,
|
|
1177
|
+
assertNoFormat2CompositeFields: () => assertNoFormat2CompositeFields,
|
|
649
1178
|
buildSafeAlias: () => buildSafeAlias,
|
|
650
1179
|
checkAutoStampsReservedFields: () => checkAutoStampsReservedFields,
|
|
651
1180
|
checkReservedFieldName: () => checkReservedFieldName,
|
|
652
1181
|
clearMetaSyncCache: () => clearMetaSyncCache,
|
|
1182
|
+
completeOverlayEntries: () => completeOverlayEntries,
|
|
653
1183
|
createDatabaseDO: () => createDatabaseDO,
|
|
654
1184
|
createDocumentDO: () => createDocumentDO,
|
|
1185
|
+
decodeOverlaySegment: () => decodeOverlaySegment,
|
|
655
1186
|
deterministicIdentifierHash: () => deterministicIdentifierHash,
|
|
1187
|
+
discoverDeclaredModel: () => discoverDeclaredModel,
|
|
656
1188
|
discoverModelNames: () => discoverModelNames,
|
|
657
1189
|
discoverSchema: () => discoverSchema,
|
|
1190
|
+
encodeOverlayMutation: () => encodeOverlayMutation,
|
|
1191
|
+
encodeOverlaySegment: () => encodeOverlaySegment,
|
|
1192
|
+
encodeSnapshotChunk: () => encodeSnapshotChunk,
|
|
1193
|
+
format2StringSetMembers: () => format2StringSetMembers,
|
|
1194
|
+
groupOverlayEntries: () => groupOverlayEntries,
|
|
658
1195
|
handleRequest: () => handleRequest,
|
|
659
1196
|
inferFieldType: () => inferFieldType,
|
|
1197
|
+
isFormat2StringSetYMap: () => isFormat2StringSetYMap,
|
|
660
1198
|
loadSchemaFromTomlString: () => loadSchemaFromTomlString,
|
|
1199
|
+
materializeOverlayRecord: () => materializeOverlayRecord,
|
|
1200
|
+
overlayFieldKey: () => overlayFieldKey,
|
|
1201
|
+
overlayMarkerKey: () => overlayMarkerKey,
|
|
1202
|
+
overlayMemberKey: () => overlayMemberKey,
|
|
1203
|
+
parseOverlayKey: () => parseOverlayKey,
|
|
661
1204
|
parseRecordRow: () => parseRecordRow,
|
|
1205
|
+
projectOverlayEntry: () => projectOverlayEntry,
|
|
662
1206
|
projectedJsonAlias: () => projectedJsonAlias,
|
|
1207
|
+
readOverlayRecord: () => readOverlayRecord,
|
|
1208
|
+
readSnapshotChunk: () => readSnapshotChunk,
|
|
663
1209
|
registerFunctionDefault: () => registerFunctionDefault,
|
|
664
1210
|
schemaToToml: () => schemaToToml,
|
|
1211
|
+
snapshotChunkLines: () => snapshotChunkLines,
|
|
1212
|
+
snapshotDeclaredSchema: () => snapshotDeclaredSchema,
|
|
1213
|
+
snapshotRowAsOverlayEntry: () => snapshotRowAsOverlayEntry,
|
|
1214
|
+
snapshotStringSetFields: () => snapshotStringSetFields,
|
|
665
1215
|
syncInferredMeta: () => syncInferredMeta,
|
|
666
1216
|
syncModelMeta: () => syncModelMeta
|
|
667
1217
|
});
|
|
@@ -699,6 +1249,32 @@ var DatabaseEngine = class {
|
|
|
699
1249
|
getTableName(_modelName) {
|
|
700
1250
|
throw new Error("Method not implemented.");
|
|
701
1251
|
}
|
|
1252
|
+
/**
|
|
1253
|
+
* A SYNCHRONOUS SQL executor over this engine's database, used by the
|
|
1254
|
+
* large-document (format 2) record store. That store commits a merged
|
|
1255
|
+
* record row and its pending-operation entry as one transaction — the
|
|
1256
|
+
* barrier a `save()` resolves behind — which the async `query()` API cannot
|
|
1257
|
+
* express. Engines whose underlying SQLite is synchronous (better-sqlite3,
|
|
1258
|
+
* SQL.js) provide it; anything else leaves it undefined and cannot host a
|
|
1259
|
+
* large document.
|
|
1260
|
+
*/
|
|
1261
|
+
createFormat2Sql;
|
|
1262
|
+
/**
|
|
1263
|
+
* Whether what {@link createFormat2Sql} writes survives this process.
|
|
1264
|
+
*
|
|
1265
|
+
* On a large document the merged record view IS the document: the Y.Doc
|
|
1266
|
+
* holds one epoch's overlay, and everything else — including a record a
|
|
1267
|
+
* `save()` has already resolved, and the pending write that still owes it to
|
|
1268
|
+
* the server — lives only in this store. An engine whose database is in
|
|
1269
|
+
* memory therefore cannot host one: a reload does not cost a cache, it costs
|
|
1270
|
+
* the document and the writes nobody has acknowledged yet.
|
|
1271
|
+
*
|
|
1272
|
+
* The platform is asked separately about its storage quota; this answers the
|
|
1273
|
+
* half only the engine knows. An engine that does not implement it is taken
|
|
1274
|
+
* at the platform's word, which is what keeps hosts with storage of their
|
|
1275
|
+
* own — a worker handed a file store, a test — working unchanged.
|
|
1276
|
+
*/
|
|
1277
|
+
format2StorageIsDurable;
|
|
702
1278
|
// Transaction support
|
|
703
1279
|
async withTransaction(_callback) {
|
|
704
1280
|
throw new Error("Method not implemented.");
|
|
@@ -2032,8 +2608,11 @@ var JsonQueryTranslator = class {
|
|
|
2032
2608
|
};
|
|
2033
2609
|
case "$ne":
|
|
2034
2610
|
this.validateFieldValue(fieldName, value);
|
|
2611
|
+
if (value === null || value === void 0) {
|
|
2612
|
+
return { sql: `${fieldSql} IS NOT NULL`, params: [] };
|
|
2613
|
+
}
|
|
2035
2614
|
return {
|
|
2036
|
-
sql:
|
|
2615
|
+
sql: `(${fieldSql} IS NULL OR ${fieldSql} != ?)`,
|
|
2037
2616
|
params: [this.convertValueForSQLite(value)]
|
|
2038
2617
|
};
|
|
2039
2618
|
case "$gt":
|
|
@@ -2116,12 +2695,23 @@ var JsonQueryTranslator = class {
|
|
|
2116
2695
|
if (value.length === 0) {
|
|
2117
2696
|
return { sql: "", params: [] };
|
|
2118
2697
|
}
|
|
2119
|
-
const
|
|
2120
|
-
const
|
|
2698
|
+
const ninHasNull = value.some((v) => v === null || v === void 0);
|
|
2699
|
+
const ninValues = value.filter((v) => v !== null && v !== void 0);
|
|
2700
|
+
const ninPlaceholders = ninValues.map(() => "?").join(",");
|
|
2701
|
+
const convertedNinValues = ninValues.map(
|
|
2121
2702
|
(v) => this.convertValueForSQLite(v)
|
|
2122
2703
|
);
|
|
2704
|
+
if (ninHasNull) {
|
|
2705
|
+
if (ninValues.length === 0) {
|
|
2706
|
+
return { sql: `${fieldSql} IS NOT NULL`, params: [] };
|
|
2707
|
+
}
|
|
2708
|
+
return {
|
|
2709
|
+
sql: `(${fieldSql} IS NOT NULL AND ${fieldSql} NOT IN (${ninPlaceholders}))`,
|
|
2710
|
+
params: convertedNinValues
|
|
2711
|
+
};
|
|
2712
|
+
}
|
|
2123
2713
|
return {
|
|
2124
|
-
sql:
|
|
2714
|
+
sql: `(${fieldSql} IS NULL OR ${fieldSql} NOT IN (${ninPlaceholders}))`,
|
|
2125
2715
|
params: convertedNinValues
|
|
2126
2716
|
};
|
|
2127
2717
|
case "$exists":
|
|
@@ -4454,6 +5044,12 @@ function discoverSchema(yDoc) {
|
|
|
4454
5044
|
}
|
|
4455
5045
|
return { models };
|
|
4456
5046
|
}
|
|
5047
|
+
function discoverDeclaredModel(yDoc, modelName) {
|
|
5048
|
+
const key = `_meta_${modelName}`;
|
|
5049
|
+
if (!yDoc.share.has(key)) return void 0;
|
|
5050
|
+
const map = materializeMap(yDoc, key);
|
|
5051
|
+
return map ? readModelMeta(map) : void 0;
|
|
5052
|
+
}
|
|
4457
5053
|
function discoverModelNames(yDoc) {
|
|
4458
5054
|
const names = [];
|
|
4459
5055
|
for (const key of yDoc.share.keys()) {
|
|
@@ -4932,3 +5528,248 @@ function loadSchemaFromTomlString(tomlString, options = {}) {
|
|
|
4932
5528
|
// src/cloudflare-do.ts
|
|
4933
5529
|
init_metaSync();
|
|
4934
5530
|
init_sql();
|
|
5531
|
+
init_largeDocuments();
|
|
5532
|
+
|
|
5533
|
+
// src/utils/overlaySql.ts
|
|
5534
|
+
init_sql();
|
|
5535
|
+
var DEFAULT_OVERLAY_TABLES = {
|
|
5536
|
+
records: "records",
|
|
5537
|
+
stringSetIndex: "stringset_index"
|
|
5538
|
+
};
|
|
5539
|
+
var statementCache = /* @__PURE__ */ new Map();
|
|
5540
|
+
function statementsFor(tables) {
|
|
5541
|
+
const cacheKey = `${tables.records}\0${tables.stringSetIndex}`;
|
|
5542
|
+
const cached = statementCache.get(cacheKey);
|
|
5543
|
+
if (cached) return cached;
|
|
5544
|
+
assertValidIdentifier(tables.records, "overlay records table");
|
|
5545
|
+
assertValidIdentifier(tables.stringSetIndex, "overlay stringset table");
|
|
5546
|
+
const records = tables.records;
|
|
5547
|
+
const members = tables.stringSetIndex;
|
|
5548
|
+
const statements = {
|
|
5549
|
+
deleteRecord: `DELETE FROM ${records} WHERE _type = ? AND _id = ?`,
|
|
5550
|
+
deleteAllMembers: `DELETE FROM ${members} WHERE _type = ? AND _record_id = ?`,
|
|
5551
|
+
deleteMember: `DELETE FROM ${members} WHERE _type = ? AND _record_id = ? AND field = ? AND value = ?`,
|
|
5552
|
+
insertMember: `INSERT OR IGNORE INTO ${members} (_record_id, _type, field, value) VALUES (?, ?, ?, ?)`,
|
|
5553
|
+
selectMembers: `SELECT value FROM ${members} WHERE _type = ? AND _record_id = ? AND field = ? ORDER BY rowid`,
|
|
5554
|
+
mergeUpsert: `
|
|
5555
|
+
INSERT INTO ${records} (_id, _type, _data)
|
|
5556
|
+
VALUES (?, ?, json_patch('{}', ?))
|
|
5557
|
+
ON CONFLICT(_type, _id) DO UPDATE SET
|
|
5558
|
+
_data = json_patch(${records}._data, ?)`.trim(),
|
|
5559
|
+
replaceUpsert: `
|
|
5560
|
+
INSERT INTO ${records} (_id, _type, _data)
|
|
5561
|
+
VALUES (?, ?, json_patch('{}', ?))
|
|
5562
|
+
ON CONFLICT(_type, _id) DO UPDATE SET
|
|
5563
|
+
_data = json_patch('{}', ?)`.trim()
|
|
5564
|
+
};
|
|
5565
|
+
statementCache.set(cacheKey, statements);
|
|
5566
|
+
return statements;
|
|
5567
|
+
}
|
|
5568
|
+
function projectOverlayEntry(sql, modelName, entry, tables = DEFAULT_OVERLAY_TABLES) {
|
|
5569
|
+
const id = entry.id;
|
|
5570
|
+
const {
|
|
5571
|
+
deleteRecord: DELETE_RECORD,
|
|
5572
|
+
deleteAllMembers: DELETE_ALL_MEMBERS,
|
|
5573
|
+
deleteMember: DELETE_MEMBER,
|
|
5574
|
+
insertMember: INSERT_MEMBER,
|
|
5575
|
+
selectMembers: SELECT_MEMBERS,
|
|
5576
|
+
mergeUpsert: MERGE_UPSERT,
|
|
5577
|
+
replaceUpsert: REPLACE_UPSERT
|
|
5578
|
+
} = statementsFor(tables);
|
|
5579
|
+
if (entry.deleted) {
|
|
5580
|
+
sql.exec(DELETE_RECORD, modelName, id);
|
|
5581
|
+
sql.exec(DELETE_ALL_MEMBERS, modelName, id);
|
|
5582
|
+
return;
|
|
5583
|
+
}
|
|
5584
|
+
const touchedFields = Object.keys(entry.fields);
|
|
5585
|
+
const touchedSets = Object.keys(entry.stringSets);
|
|
5586
|
+
if (!entry.replace && touchedFields.length === 0 && touchedSets.length === 0) {
|
|
5587
|
+
return;
|
|
5588
|
+
}
|
|
5589
|
+
if (entry.replace) {
|
|
5590
|
+
sql.exec(DELETE_ALL_MEMBERS, modelName, id);
|
|
5591
|
+
}
|
|
5592
|
+
const patch = { ...entry.fields, id };
|
|
5593
|
+
for (const [field, members] of Object.entries(entry.stringSets)) {
|
|
5594
|
+
for (const [member, present] of Object.entries(members)) {
|
|
5595
|
+
if (present) sql.exec(INSERT_MEMBER, id, modelName, field, member);
|
|
5596
|
+
else sql.exec(DELETE_MEMBER, modelName, id, field, member);
|
|
5597
|
+
}
|
|
5598
|
+
const rows = Array.from(sql.exec(SELECT_MEMBERS, modelName, id, field));
|
|
5599
|
+
patch[field] = rows.map((row) => row.value);
|
|
5600
|
+
}
|
|
5601
|
+
const patchJson = JSON.stringify(patch);
|
|
5602
|
+
sql.exec(
|
|
5603
|
+
entry.replace ? REPLACE_UPSERT : MERGE_UPSERT,
|
|
5604
|
+
id,
|
|
5605
|
+
modelName,
|
|
5606
|
+
patchJson,
|
|
5607
|
+
patchJson
|
|
5608
|
+
);
|
|
5609
|
+
}
|
|
5610
|
+
|
|
5611
|
+
// src/utils/snapshotChunks.ts
|
|
5612
|
+
var SnapshotChunkIntegrityError = class extends Error {
|
|
5613
|
+
constructor(key, reason) {
|
|
5614
|
+
super(`Snapshot chunk ${key} does not match its manifest entry: ${reason}`);
|
|
5615
|
+
this.key = key;
|
|
5616
|
+
this.name = "SnapshotChunkIntegrityError";
|
|
5617
|
+
}
|
|
5618
|
+
};
|
|
5619
|
+
var SnapshotRowFormatError = class extends Error {
|
|
5620
|
+
constructor(model, id) {
|
|
5621
|
+
super(
|
|
5622
|
+
`Record ${model}/${id} cannot be written to a snapshot chunk: its id or data contains a newline or tab, which the line format reserves`
|
|
5623
|
+
);
|
|
5624
|
+
this.name = "SnapshotRowFormatError";
|
|
5625
|
+
}
|
|
5626
|
+
};
|
|
5627
|
+
function snapshotStringSetFields(manifest, model) {
|
|
5628
|
+
const declared = manifest.schema?.[model]?.stringSetFields;
|
|
5629
|
+
return new Set(Array.isArray(declared) ? declared.map(String) : []);
|
|
5630
|
+
}
|
|
5631
|
+
function snapshotDeclaredSchema(manifest, model) {
|
|
5632
|
+
const declared = manifest.schema?.[model] ?? {};
|
|
5633
|
+
const list = (value) => Array.isArray(value) ? value : [];
|
|
5634
|
+
const declaration = declared.declaration && typeof declared.declaration === "object" ? {
|
|
5635
|
+
fields: declared.declaration.fields ?? {},
|
|
5636
|
+
constraints: declared.declaration.constraints ?? {}
|
|
5637
|
+
} : void 0;
|
|
5638
|
+
return {
|
|
5639
|
+
...declaration ? { declaration } : {},
|
|
5640
|
+
stringSetFields: list(declared.stringSetFields).map(String),
|
|
5641
|
+
uniqueConstraints: list(declared.uniqueConstraints).filter(
|
|
5642
|
+
(constraint) => constraint?.name && Array.isArray(constraint.fields) && constraint.fields.length > 0
|
|
5643
|
+
).map((constraint) => ({
|
|
5644
|
+
name: String(constraint.name),
|
|
5645
|
+
fields: constraint.fields.map(String)
|
|
5646
|
+
})),
|
|
5647
|
+
indexedFields: list(declared.indexedFields).filter((index) => index?.field).map((index) => ({
|
|
5648
|
+
field: String(index.field),
|
|
5649
|
+
type: String(index.type || "string"),
|
|
5650
|
+
unique: !!index.unique
|
|
5651
|
+
}))
|
|
5652
|
+
};
|
|
5653
|
+
}
|
|
5654
|
+
function snapshotRowAsOverlayEntry(id, data, stringSetFields) {
|
|
5655
|
+
const fields = {};
|
|
5656
|
+
const stringSets = {};
|
|
5657
|
+
for (const [field, value] of Object.entries(data)) {
|
|
5658
|
+
if (field === "id") continue;
|
|
5659
|
+
if (stringSetFields.has(field) && Array.isArray(value)) {
|
|
5660
|
+
const members = {};
|
|
5661
|
+
for (const member of value) members[String(member)] = true;
|
|
5662
|
+
stringSets[field] = members;
|
|
5663
|
+
continue;
|
|
5664
|
+
}
|
|
5665
|
+
fields[field] = value;
|
|
5666
|
+
}
|
|
5667
|
+
return { id, fields, stringSets, replace: true, deleted: false };
|
|
5668
|
+
}
|
|
5669
|
+
var encoder = new TextEncoder();
|
|
5670
|
+
var decoder = new TextDecoder();
|
|
5671
|
+
function assertLineSafe(row) {
|
|
5672
|
+
if (/[\n\r\t]/.test(row._id) || /[\n\r\t]/.test(row._data)) {
|
|
5673
|
+
throw new SnapshotRowFormatError(row._type, row._id);
|
|
5674
|
+
}
|
|
5675
|
+
}
|
|
5676
|
+
function snapshotChunkLines(rows) {
|
|
5677
|
+
let body = "";
|
|
5678
|
+
for (const row of rows) {
|
|
5679
|
+
assertLineSafe(row);
|
|
5680
|
+
body += `${row._id} ${row._data}
|
|
5681
|
+
`;
|
|
5682
|
+
}
|
|
5683
|
+
return body;
|
|
5684
|
+
}
|
|
5685
|
+
async function gzip(bytes) {
|
|
5686
|
+
return await collect(
|
|
5687
|
+
new Blob([bytes]).stream().pipeThrough(new CompressionStream("gzip"))
|
|
5688
|
+
);
|
|
5689
|
+
}
|
|
5690
|
+
async function gunzip(bytes) {
|
|
5691
|
+
return await collect(
|
|
5692
|
+
new Blob([bytes]).stream().pipeThrough(new DecompressionStream("gzip"))
|
|
5693
|
+
);
|
|
5694
|
+
}
|
|
5695
|
+
async function collect(stream) {
|
|
5696
|
+
const parts = [];
|
|
5697
|
+
let total = 0;
|
|
5698
|
+
const reader = stream.getReader();
|
|
5699
|
+
for (; ; ) {
|
|
5700
|
+
const { done, value } = await reader.read();
|
|
5701
|
+
if (done) break;
|
|
5702
|
+
parts.push(value);
|
|
5703
|
+
total += value.byteLength;
|
|
5704
|
+
}
|
|
5705
|
+
const out = new Uint8Array(total);
|
|
5706
|
+
let at = 0;
|
|
5707
|
+
for (const part of parts) {
|
|
5708
|
+
out.set(part, at);
|
|
5709
|
+
at += part.byteLength;
|
|
5710
|
+
}
|
|
5711
|
+
return out;
|
|
5712
|
+
}
|
|
5713
|
+
async function sha256Hex(bytes) {
|
|
5714
|
+
const digest = await crypto.subtle.digest("SHA-256", bytes);
|
|
5715
|
+
return Array.from(new Uint8Array(digest)).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
5716
|
+
}
|
|
5717
|
+
async function encodeSnapshotChunk(input) {
|
|
5718
|
+
const lines = encoder.encode(snapshotChunkLines(input.rows));
|
|
5719
|
+
const body = await gzip(lines);
|
|
5720
|
+
return {
|
|
5721
|
+
body,
|
|
5722
|
+
entry: {
|
|
5723
|
+
key: input.key,
|
|
5724
|
+
model: input.model,
|
|
5725
|
+
bytes: body.byteLength,
|
|
5726
|
+
rawBytes: lines.byteLength,
|
|
5727
|
+
rows: input.rows.length,
|
|
5728
|
+
firstId: input.rows.length > 0 ? input.rows[0]._id : "",
|
|
5729
|
+
lastId: input.rows.length > 0 ? input.rows[input.rows.length - 1]._id : "",
|
|
5730
|
+
sha256: await sha256Hex(body)
|
|
5731
|
+
}
|
|
5732
|
+
};
|
|
5733
|
+
}
|
|
5734
|
+
async function readSnapshotChunk(body, descriptor) {
|
|
5735
|
+
const entry = "sha256" in descriptor ? descriptor : null;
|
|
5736
|
+
if (entry) {
|
|
5737
|
+
if (body.byteLength !== entry.bytes) {
|
|
5738
|
+
throw new SnapshotChunkIntegrityError(
|
|
5739
|
+
entry.key,
|
|
5740
|
+
`expected ${entry.bytes} bytes, got ${body.byteLength}`
|
|
5741
|
+
);
|
|
5742
|
+
}
|
|
5743
|
+
const digest = await sha256Hex(body);
|
|
5744
|
+
if (digest !== entry.sha256) {
|
|
5745
|
+
throw new SnapshotChunkIntegrityError(
|
|
5746
|
+
entry.key,
|
|
5747
|
+
`expected sha256 ${entry.sha256}, got ${digest}`
|
|
5748
|
+
);
|
|
5749
|
+
}
|
|
5750
|
+
}
|
|
5751
|
+
const text = decoder.decode(await gunzip(body));
|
|
5752
|
+
const rows = [];
|
|
5753
|
+
for (const line of text.split("\n")) {
|
|
5754
|
+
if (line.length === 0) continue;
|
|
5755
|
+
const tab = line.indexOf(" ");
|
|
5756
|
+
if (tab < 0) {
|
|
5757
|
+
throw new SnapshotChunkIntegrityError(
|
|
5758
|
+
entry?.key ?? descriptor.model,
|
|
5759
|
+
"a line carries no id/data separator"
|
|
5760
|
+
);
|
|
5761
|
+
}
|
|
5762
|
+
rows.push({
|
|
5763
|
+
_type: descriptor.model,
|
|
5764
|
+
_id: line.slice(0, tab),
|
|
5765
|
+
_data: line.slice(tab + 1)
|
|
5766
|
+
});
|
|
5767
|
+
}
|
|
5768
|
+
if (entry && rows.length !== entry.rows) {
|
|
5769
|
+
throw new SnapshotChunkIntegrityError(
|
|
5770
|
+
entry.key,
|
|
5771
|
+
`expected ${entry.rows} rows, got ${rows.length}`
|
|
5772
|
+
);
|
|
5773
|
+
}
|
|
5774
|
+
return rows;
|
|
5775
|
+
}
|