@tldraw/store
Version:
tldraw infinite canvas SDK (store).
347 lines (346 loc) • 11.8 kB
JavaScript
import {
computed,
EMPTY_ARRAY,
isUninitialized,
RESET_VALUE,
withDiff
} from "@tldraw/state";
import { areArraysShallowEqual, isEqual, objectMapValues } from "@tldraw/utils";
import { executeQuery, objectMatchesQuery } from "./executeQuery.mjs";
import { IncrementalSetConstructor } from "./IncrementalSetConstructor.mjs";
import { diffSets } from "./setUtils.mjs";
class StoreQueries {
constructor(recordMap, history) {
this.recordMap = recordMap;
this.history = history;
}
/**
* A cache of derivations (indexes).
*
* @internal
*/
indexCache = /* @__PURE__ */ new Map();
/**
* A cache of derivations (filtered histories).
*
* @internal
*/
historyCache = /* @__PURE__ */ new Map();
/**
* Create a derivation that contains the history for a given type
*
* @param typeName - The name of the type to filter by.
* @returns A derivation that returns the ids of all records of the given type.
* @public
*/
filterHistory(typeName) {
if (this.historyCache.has(typeName)) {
return this.historyCache.get(typeName);
}
const filtered = computed(
"filterHistory:" + typeName,
(lastValue, lastComputedEpoch) => {
if (isUninitialized(lastValue)) {
return this.history.get();
}
const diff = this.history.getDiffSince(lastComputedEpoch);
if (diff === RESET_VALUE) return this.history.get();
const res = { added: {}, removed: {}, updated: {} };
let numAdded = 0;
let numRemoved = 0;
let numUpdated = 0;
for (const changes of diff) {
for (const added of objectMapValues(changes.added)) {
if (added.typeName === typeName) {
if (res.removed[added.id]) {
const original = res.removed[added.id];
delete res.removed[added.id];
numRemoved--;
if (original !== added) {
res.updated[added.id] = [original, added];
numUpdated++;
}
} else {
res.added[added.id] = added;
numAdded++;
}
}
}
for (const [from, to] of objectMapValues(changes.updated)) {
if (to.typeName === typeName) {
if (res.added[to.id]) {
res.added[to.id] = to;
} else if (res.updated[to.id]) {
res.updated[to.id] = [res.updated[to.id][0], to];
} else {
res.updated[to.id] = [from, to];
numUpdated++;
}
}
}
for (const removed of objectMapValues(changes.removed)) {
if (removed.typeName === typeName) {
if (res.added[removed.id]) {
delete res.added[removed.id];
numAdded--;
} else if (res.updated[removed.id]) {
res.removed[removed.id] = res.updated[removed.id][0];
delete res.updated[removed.id];
numUpdated--;
numRemoved++;
} else {
res.removed[removed.id] = removed;
numRemoved++;
}
}
}
}
if (numAdded || numRemoved || numUpdated) {
return withDiff(this.history.get(), res);
} else {
return lastValue;
}
},
{ historyLength: 100 }
);
this.historyCache.set(typeName, filtered);
return filtered;
}
/**
* Create a derivation that returns an index on a property for the given type.
*
* @param typeName - The name of the type.
* @param property - The name of the property.
* @public
*/
index(typeName, property) {
const cacheKey = typeName + ":" + property;
if (this.indexCache.has(cacheKey)) {
return this.indexCache.get(cacheKey);
}
const index = this.__uncached_createIndex(typeName, property);
this.indexCache.set(cacheKey, index);
return index;
}
/**
* Create a derivation that returns an index on a property for the given type.
*
* @param typeName - The name of the type?.
* @param property - The name of the property?.
* @internal
*/
__uncached_createIndex(typeName, property) {
const typeHistory = this.filterHistory(typeName);
const fromScratch = () => {
typeHistory.get();
const res = /* @__PURE__ */ new Map();
for (const record of this.recordMap.values()) {
if (record.typeName === typeName) {
const value = record[property];
if (!res.has(value)) {
res.set(value, /* @__PURE__ */ new Set());
}
res.get(value).add(record.id);
}
}
return res;
};
return computed(
"index:" + typeName + ":" + property,
(prevValue, lastComputedEpoch) => {
if (isUninitialized(prevValue)) return fromScratch();
const history = typeHistory.getDiffSince(lastComputedEpoch);
if (history === RESET_VALUE) {
return fromScratch();
}
const setConstructors = /* @__PURE__ */ new Map();
const add = (value, id) => {
let setConstructor = setConstructors.get(value);
if (!setConstructor)
setConstructor = new IncrementalSetConstructor(
prevValue.get(value) ?? /* @__PURE__ */ new Set()
);
setConstructor.add(id);
setConstructors.set(value, setConstructor);
};
const remove = (value, id) => {
let set = setConstructors.get(value);
if (!set) set = new IncrementalSetConstructor(prevValue.get(value) ?? /* @__PURE__ */ new Set());
set.remove(id);
setConstructors.set(value, set);
};
for (const changes of history) {
for (const record of objectMapValues(changes.added)) {
if (record.typeName === typeName) {
const value = record[property];
add(value, record.id);
}
}
for (const [from, to] of objectMapValues(changes.updated)) {
if (to.typeName === typeName) {
const prev = from[property];
const next = to[property];
if (prev !== next) {
remove(prev, to.id);
add(next, to.id);
}
}
}
for (const record of objectMapValues(changes.removed)) {
if (record.typeName === typeName) {
const value = record[property];
remove(value, record.id);
}
}
}
let nextValue = void 0;
let nextDiff = void 0;
for (const [value, setConstructor] of setConstructors) {
const result = setConstructor.get();
if (!result) continue;
if (!nextValue) nextValue = new Map(prevValue);
if (!nextDiff) nextDiff = /* @__PURE__ */ new Map();
if (result.value.size === 0) {
nextValue.delete(value);
} else {
nextValue.set(value, result.value);
}
nextDiff.set(value, result.diff);
}
if (nextValue && nextDiff) {
return withDiff(nextValue, nextDiff);
}
return prevValue;
},
{ historyLength: 100 }
);
}
/**
* Create a derivation that will return a signle record matching the given query.
*
* It will return undefined if there is no matching record
*
* @param typeName - The name of the type?
* @param queryCreator - A function that returns the query expression.
* @param name - (optional) The name of the query.
*/
record(typeName, queryCreator = () => ({}), name = "record:" + typeName + (queryCreator ? ":" + queryCreator.toString() : "")) {
const ids = this.ids(typeName, queryCreator, name);
return computed(name, () => {
for (const id of ids.get()) {
return this.recordMap.get(id);
}
return void 0;
});
}
/**
* Create a derivation that will return an array of records matching the given query
*
* @param typeName - The name of the type?
* @param queryCreator - A function that returns the query expression.
* @param name - (optinal) The name of the query.
*/
records(typeName, queryCreator = () => ({}), name = "records:" + typeName + (queryCreator ? ":" + queryCreator.toString() : "")) {
const ids = this.ids(typeName, queryCreator, "ids:" + name);
return computed(
name,
() => {
return Array.from(ids.get(), (id) => this.recordMap.get(id));
},
{
isEqual: areArraysShallowEqual
}
);
}
/**
* Create a derivation that will return the ids of all records of the given type.
*
* @param typeName - The name of the type.
* @param queryCreator - A function that returns the query expression.
* @param name - (optinal) The name of the query.
*/
ids(typeName, queryCreator = () => ({}), name = "ids:" + typeName + (queryCreator ? ":" + queryCreator.toString() : "")) {
const typeHistory = this.filterHistory(typeName);
const fromScratch = () => {
typeHistory.get();
const query = queryCreator();
if (Object.keys(query).length === 0) {
const ids = /* @__PURE__ */ new Set();
for (const record of this.recordMap.values()) {
if (record.typeName === typeName) ids.add(record.id);
}
return ids;
}
return executeQuery(this, typeName, query);
};
const fromScratchWithDiff = (prevValue) => {
const nextValue = fromScratch();
const diff = diffSets(prevValue, nextValue);
if (diff) {
return withDiff(nextValue, diff);
} else {
return prevValue;
}
};
const cachedQuery = computed("ids_query:" + name, queryCreator, {
isEqual
});
return computed(
"query:" + name,
(prevValue, lastComputedEpoch) => {
const query = cachedQuery.get();
if (isUninitialized(prevValue)) {
return fromScratch();
}
if (lastComputedEpoch < cachedQuery.lastChangedEpoch) {
return fromScratchWithDiff(prevValue);
}
const history = typeHistory.getDiffSince(lastComputedEpoch);
if (history === RESET_VALUE) {
return fromScratchWithDiff(prevValue);
}
const setConstructor = new IncrementalSetConstructor(
prevValue
);
for (const changes of history) {
for (const added of objectMapValues(changes.added)) {
if (added.typeName === typeName && objectMatchesQuery(query, added)) {
setConstructor.add(added.id);
}
}
for (const [_, updated] of objectMapValues(changes.updated)) {
if (updated.typeName === typeName) {
if (objectMatchesQuery(query, updated)) {
setConstructor.add(updated.id);
} else {
setConstructor.remove(updated.id);
}
}
}
for (const removed of objectMapValues(changes.removed)) {
if (removed.typeName === typeName) {
setConstructor.remove(removed.id);
}
}
}
const result = setConstructor.get();
if (!result) {
return prevValue;
}
return withDiff(result.value, result.diff);
},
{ historyLength: 50 }
);
}
exec(typeName, query) {
const ids = executeQuery(this, typeName, query);
if (ids.size === 0) {
return EMPTY_ARRAY;
}
return Array.from(ids, (id) => this.recordMap.get(id));
}
}
export {
StoreQueries
};
//# sourceMappingURL=StoreQueries.mjs.map