Skip to content

Commit b8f613c

Browse files
authored
Merge pull request #21 from RoundingWellOS/v2
feat(store): add v2 cache primitives
2 parents 0902aaf + 91e6a13 commit b8f613c

33 files changed

Lines changed: 4704 additions & 4562 deletions

.babelrc

Lines changed: 0 additions & 8 deletions
This file was deleted.

.eslintrc

Lines changed: 0 additions & 11 deletions
This file was deleted.

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
24

.travis.yml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
language: node_js
22
node_js:
3-
- "12.0"
3+
- "24"
4+
script:
5+
- npm run check

CHANGELOG.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# Changelog
2+
3+
## 2.0.0
4+
5+
- Add synchronous cache primitives: `find`, `has`, `patch`, `evict`, `inspect`, `reset`, and `resetAll`.
6+
- Ship TypeScript declarations for the public Store and ModelCache APIs.
7+
- Modernize package output to ESM (`dist/backbone.store.mjs`), CJS (`dist/backbone.store.cjs`), and UMD (`dist/backbone.store.js`) through an `exports` map.
8+
- Add `"type": "module"` for native ESM source and tooling.
9+
- Change raw CommonJS consumption to use the default export: `require('backbone.store').default`.
10+
- Remove the old `dist/backbone.store.esm.js` filename; use `dist/backbone.store.mjs` or the package root import instead.
11+
- Keep UMD assignment on `Backbone.Store` without adding a `window.Store` global.
12+
- Upgrade the test/build stack for Node 24 and replace Babel/Rollup/nyc with native ESM, tsdown, ESLint flat config, and c8.
13+
14+
### Fixes
15+
16+
- Prevent id-key collisions for ids such as `"toString"` and `"__proto__"` by using null-prototype cache storage.
17+
- Track pending no-id models so Store-owned listeners are cleaned up if the model is reset, removed, or destroyed before receiving an id.
18+
- Detach Store-owned `destroy` listeners when `Store.remove(modelName)`, `Store.removeAll()`, `Store.reset(modelName)`, or `Store.resetAll()` clears cached instances.
19+
- Avoid object-inspection conflicts when assertion/debug tools call `Store.inspect(depth, options)` while formatting Store references.

README.md

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,14 +65,49 @@ Returns all `ModelCache` instances by name.
6565
#### `Store.get(modelName)`
6666
Returns a Model definition by name.
6767

68+
#### `Store.find(modelName, id)`
69+
Returns a cached model instance by id, or `undefined` if the model is not cached.
70+
The id is normalized with `String(id)`, so `1` and `"1"` resolve to the same cached model.
71+
`null` and `undefined` ids return `undefined`.
72+
73+
#### `Store.has(modelName, id)`
74+
Returns `true` when a model instance is cached by id.
75+
`null` and `undefined` ids return `false`.
76+
77+
#### `Store.patch(modelName, id, attrs, [options])`
78+
Updates an existing cached model with `model.set(attrs, options)`.
79+
Returns the patched model, or `undefined` when the id is not cached.
80+
This is the option-aware update API; duplicate construction still preserves the original behavior and calls `set(attrs)` without options.
81+
82+
#### `Store.evict(modelName, id)`
83+
Removes one cached model without destroying it.
84+
Returns the evicted model, or `undefined` when the id is not cached.
85+
This triggers the Store `remove` event for the evicted model.
86+
87+
#### `Store.inspect(modelName, id)`
88+
Returns `{ modelName, id, key, cached, model }`.
89+
`id` is the caller-supplied id, `key` is the normalized cache key, and `model` is the live Backbone model reference or `undefined`.
90+
For `null` and `undefined` ids, `key` is `undefined`, `cached` is `false`, and `model` is `undefined`.
91+
When called by object-formatting tools as `.inspect(depth, options)`, returns a short Store label instead of treating `depth` as a model name.
92+
6893
#### `Store.getAll()`
6994
Returns all Model definitions by name.
7095

96+
#### `Store.reset(modelName)`
97+
Clears all cached and pending models for one `modelName` while keeping the registered Model definition.
98+
This detaches Store-owned listeners and does not trigger `remove` or `reset` events.
99+
100+
#### `Store.resetAll()`
101+
Clears all cached and pending models for every registered Model definition.
102+
This keeps the registered Model definitions and does not trigger `remove` or `reset` events.
103+
71104
#### `Store.remove(modelName)`
72105
Removes a `ModelCache` from `Store` by name.
106+
This detaches Store-owned listeners and does not trigger `remove` or `reset` events.
73107

74108
#### `Store.removeAll()`
75109
Removes the entire cache.
110+
This detaches Store-owned listeners and does not trigger `remove` or `reset` events.
76111

77112
#### `Store` examples
78113
```javscript
@@ -93,6 +128,9 @@ const Models = Store.getAll();
93128
console.log(StoredModel === Models.myModel.modelConstructor);
94129
```
95130

131+
All APIs that take `modelName` throw the same error as `Store.getCache(modelName)` when the model name is not recognized.
132+
Calling `StoredModel.extend(...)` still delegates to Backbone's inherited `extend`; the returned child constructor is not automatically registered with Store unless it is passed through `Store(childModel, modelName)`.
133+
96134
### Store Events
97135

98136
#### `add`
@@ -165,11 +203,15 @@ Instances will have four properties:
165203
#### `get(attrs, options)`
166204
If the instance by index is not cached it is instantiated, cached, and returned.
167205
Otherwise it returns the cached instance and sets the `attrs` on the model.
168-
The `options` passed to this method will pass through to the `new` or the `set`.
206+
The `options` passed to this method will pass through only when a new model is instantiated.
207+
Use `patch(id, attrs, options)` when cached updates need `set` options.
169208

170209
#### `remove(instance)`
171210
Removes the model instance from the cache.
172211

212+
#### `find(id)`, `has(id)`, `patch(id, attrs, [options])`, `evict(id)`, `inspect(id)`, `reset()`
213+
ModelCache-level primitives backing the Store APIs of the same names.
214+
173215
## Acknowledgments
174216

175217
Backbone.Store is heavily inspired by [Backbone.UniqueModel](https://github.com/disqus/backbone.uniquemodel) written by [Ben Vinegar](http://github.com/benvinegar)

dist/backbone.store.cjs

Lines changed: 238 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
Object.defineProperties(exports, {
2+
__esModule: { value: true },
3+
[Symbol.toStringTag]: { value: "Module" }
4+
});
5+
//#region \0rolldown/runtime.js
6+
var __create = Object.create;
7+
var __defProp = Object.defineProperty;
8+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
9+
var __getOwnPropNames = Object.getOwnPropertyNames;
10+
var __getProtoOf = Object.getPrototypeOf;
11+
var __hasOwnProp = Object.prototype.hasOwnProperty;
12+
var __copyProps = (to, from, except, desc) => {
13+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
14+
key = keys[i];
15+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
16+
get: ((k) => from[k]).bind(null, key),
17+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
18+
});
19+
}
20+
return to;
21+
};
22+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
23+
value: mod,
24+
enumerable: true
25+
}) : target, mod));
26+
//#endregion
27+
let underscore = require("underscore");
28+
let backbone = require("backbone");
29+
backbone = __toESM(backbone, 1);
30+
//#region lib/model-cache.js
31+
function createCache() {
32+
return Object.create(null);
33+
}
34+
function ModelCache(Model, modelName) {
35+
this.instances = createCache();
36+
this.pending = [];
37+
this.Model = Model;
38+
this.modelName = modelName;
39+
this.ModelConstructor = this._getConstructor(Model);
40+
}
41+
(0, underscore.extend)(ModelCache.prototype, {
42+
_getConstructor(Model) {
43+
const cache = this;
44+
const ModelConstructor = function(attrs, options) {
45+
return cache.get(attrs, options);
46+
};
47+
(0, underscore.extend)(ModelConstructor, Model);
48+
ModelConstructor.prototype = this.Model.prototype;
49+
return ModelConstructor;
50+
},
51+
get(attrs, options) {
52+
const instanceKey = this._getKey(attrs);
53+
const instance = this.find(instanceKey);
54+
if (!instance) return this._new(attrs, options);
55+
instance.set(attrs);
56+
Store.trigger("update", instance, this);
57+
return instance;
58+
},
59+
find(id) {
60+
const key = this._key(id);
61+
if (key == null) return;
62+
return this.instances[key];
63+
},
64+
has(id) {
65+
return !!this.find(id);
66+
},
67+
patch(id, attrs, options) {
68+
const instance = this.find(id);
69+
if (!instance) return;
70+
instance.set(attrs, options);
71+
Store.trigger("update", instance, this);
72+
return instance;
73+
},
74+
evict(id) {
75+
return this._removeKey(this._key(id));
76+
},
77+
inspect(id) {
78+
const key = this._key(id);
79+
const model = key == null ? void 0 : this.instances[key];
80+
return {
81+
modelName: this.modelName,
82+
id,
83+
key,
84+
cached: !!model,
85+
model
86+
};
87+
},
88+
reset() {
89+
Object.keys(this.instances).forEach((key) => {
90+
this._detachCached(this.instances[key]);
91+
});
92+
this.pending.slice().forEach((instance) => {
93+
this._detachPending(instance);
94+
});
95+
this.instances = createCache();
96+
this.pending = [];
97+
},
98+
_new(attrs, options) {
99+
const instance = new this.Model(attrs, options);
100+
if (instance.isNew()) this._trackPending(instance);
101+
else this._add(instance);
102+
return instance;
103+
},
104+
_add(instance) {
105+
const key = this._getModelKey(instance);
106+
this._detachPending(instance);
107+
if (key == null) return;
108+
if (this.instances[key]) return;
109+
this.instances[key] = instance;
110+
this._attachCached(instance);
111+
Store.trigger("add", instance, this);
112+
},
113+
remove(instance) {
114+
this._removeKey(this._getModelKey(instance), instance);
115+
return instance;
116+
},
117+
_getKey(attrs) {
118+
return attrs && this._key(attrs[this.Model.prototype.idAttribute]);
119+
},
120+
_getModelKey(instance) {
121+
if (!instance) return;
122+
return this._key(instance.id);
123+
},
124+
_key(id) {
125+
if (id == null) return;
126+
return String(id);
127+
},
128+
_trackPending(instance) {
129+
this.pending.push(instance);
130+
instance.once(`change:${instance.idAttribute}`, this._add, this);
131+
instance.on("destroy", this._removePending, this);
132+
},
133+
_removePending(instance) {
134+
this._detachPending(instance);
135+
},
136+
_detachPending(instance) {
137+
const index = this.pending.indexOf(instance);
138+
if (index > -1) this.pending.splice(index, 1);
139+
instance.off(`change:${instance.idAttribute}`, this._add, this);
140+
instance.off("destroy", this._removePending, this);
141+
},
142+
_attachCached(instance) {
143+
instance.on("destroy", this.remove, this);
144+
},
145+
_detachCached(instance) {
146+
instance.off("destroy", this.remove, this);
147+
},
148+
_removeKey(key, eventInstance) {
149+
if (key == null) return;
150+
const instance = this.instances[key];
151+
if (!instance) return;
152+
delete this.instances[key];
153+
this._detachCached(instance);
154+
Store.trigger("remove", eventInstance || instance, this);
155+
return instance;
156+
}
157+
});
158+
//#endregion
159+
//#region lib/index.js
160+
let ModelCaches = {};
161+
const STORE_INSPECT_LABEL = "[Backbone.Store]";
162+
const nodeInspectSymbol = typeof Symbol === "function" && Symbol.for && Symbol.for("nodejs.util.inspect.custom");
163+
function isCustomInspectCall(modelName, id) {
164+
return typeof modelName === "number" && (id == null || typeof id === "object");
165+
}
166+
/**
167+
* Store wrapper converts regular Backbone models into unique ones.
168+
*
169+
* Example:
170+
* const StoredUser = Store(User);
171+
*/
172+
function Store(Model, modelName = (0, underscore.uniqueId)("Store_")) {
173+
return Store.add(Model, modelName).ModelConstructor;
174+
}
175+
(0, underscore.extend)(Store, backbone.default.Events, {
176+
ModelCache,
177+
add(Model, modelName) {
178+
if (!modelName) throw "Model name required";
179+
if (ModelCaches[modelName]) return ModelCaches[modelName];
180+
return ModelCaches[modelName] = new Store.ModelCache(Model, modelName);
181+
},
182+
getCache(modelName) {
183+
if (!ModelCaches[modelName]) throw `Unrecognized Model: "${modelName}"`;
184+
return ModelCaches[modelName];
185+
},
186+
getAllCache() {
187+
return (0, underscore.clone)(ModelCaches);
188+
},
189+
get(modelName) {
190+
return Store.getCache(modelName).ModelConstructor;
191+
},
192+
find(modelName, id) {
193+
return Store.getCache(modelName).find(id);
194+
},
195+
has(modelName, id) {
196+
return Store.getCache(modelName).has(id);
197+
},
198+
patch(modelName, id, attrs, options) {
199+
return Store.getCache(modelName).patch(id, attrs, options);
200+
},
201+
evict(modelName, id) {
202+
return Store.getCache(modelName).evict(id);
203+
},
204+
inspect(modelName, id) {
205+
if (isCustomInspectCall(modelName, id)) return STORE_INSPECT_LABEL;
206+
return Store.getCache(modelName).inspect(id);
207+
},
208+
getAll() {
209+
return (0, underscore.reduce)(ModelCaches, (all, cache, modelName) => {
210+
all[modelName] = cache.ModelConstructor;
211+
return all;
212+
}, {});
213+
},
214+
reset(modelName) {
215+
Store.getCache(modelName).reset();
216+
},
217+
resetAll() {
218+
(0, underscore.each)(ModelCaches, (cache) => {
219+
cache.reset();
220+
});
221+
},
222+
remove(modelName) {
223+
if (!ModelCaches[modelName]) return;
224+
ModelCaches[modelName].reset();
225+
delete ModelCaches[modelName];
226+
},
227+
removeAll() {
228+
Store.resetAll();
229+
ModelCaches = {};
230+
}
231+
});
232+
if (nodeInspectSymbol) Store[nodeInspectSymbol] = () => STORE_INSPECT_LABEL;
233+
backbone.default.Store = Store;
234+
//#endregion
235+
exports.ModelCache = ModelCache;
236+
exports.default = Store;
237+
238+
//# sourceMappingURL=backbone.store.cjs.map

0 commit comments

Comments
 (0)