opshot 0.5.2 → 0.6.0
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 +83 -24
- package/dist/chunk-4JZIOHSP.js +768 -0
- package/dist/createMutableState-DJ9dmpR-.d.ts +36 -0
- package/dist/index.d.ts +52 -76
- package/dist/index.js +7 -1254
- package/dist/react.d.ts +23 -0
- package/dist/react.js +517 -0
- package/package.json +10 -1
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ComponentType, FC } from 'react';
|
|
2
|
+
import { M as MutableStateOptions } from './createMutableState-DJ9dmpR-.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Wraps a component so it re-renders only when fields it read change.
|
|
6
|
+
*
|
|
7
|
+
* @typeParam P - Props type.
|
|
8
|
+
* @param Component - Component to wrap.
|
|
9
|
+
* @returns The wrapped component.
|
|
10
|
+
*/
|
|
11
|
+
declare function scope<P extends object>(Component: ComponentType<P>): FC<P>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates mutable state for a component.
|
|
15
|
+
*
|
|
16
|
+
* @typeParam T - State shape.
|
|
17
|
+
* @param properties - Initial fields, or a function that returns them.
|
|
18
|
+
* @param options - Creation options.
|
|
19
|
+
* @returns The state.
|
|
20
|
+
*/
|
|
21
|
+
declare function useMutableState<T extends object>(properties: (() => T) | T, options?: MutableStateOptions): T;
|
|
22
|
+
|
|
23
|
+
export { scope, useMutableState };
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
import { createMutableState, handlesOf, subscribe, proxyOf, isState, isSameIdentity, rawOf, addressOf, versionOf, isDangerousKind, classifyValue, peelReadProxy, recordOf, registerReadProxyTarget, walkDataEntries, isTrackedEntry, isObjectLike } from './chunk-4JZIOHSP.js';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
import { memo, useState, useReducer, useRef, useEffect, createElement, useLayoutEffect } from 'react';
|
|
4
|
+
|
|
5
|
+
// src/react/propWalk.ts
|
|
6
|
+
var stateKeysByContainer = /* @__PURE__ */ new WeakMap();
|
|
7
|
+
var noStateKeys = /* @__PURE__ */ new Set();
|
|
8
|
+
var isReactOwnNode = (value) => "$$typeof" in value || typeof Node !== "undefined" && value instanceof Node;
|
|
9
|
+
var canRebuild = (container) => !isDangerousKind(classifyValue(container));
|
|
10
|
+
var childRole = (value, writable, mode) => {
|
|
11
|
+
if (typeof value !== "object" || value === null) return "skip";
|
|
12
|
+
if (isReactOwnNode(value)) return "skip";
|
|
13
|
+
if (!isTrackedEntry(value, mode === "entry" || writable)) return "skip";
|
|
14
|
+
if (isState(value)) return "state";
|
|
15
|
+
if (canRebuild(value)) return "descend";
|
|
16
|
+
return "skip";
|
|
17
|
+
};
|
|
18
|
+
var readVerdict = (container, pass) => pass.verdicts.get(container) ?? stateKeysByContainer.get(container) ?? noStateKeys;
|
|
19
|
+
var createDiscoveryPass = () => ({
|
|
20
|
+
entriesByContainer: /* @__PURE__ */ new Map(),
|
|
21
|
+
verdicts: /* @__PURE__ */ new Map(),
|
|
22
|
+
inProgress: /* @__PURE__ */ new Set(),
|
|
23
|
+
relaxable: /* @__PURE__ */ new Set()
|
|
24
|
+
});
|
|
25
|
+
function visitContainer(container, pass, mode) {
|
|
26
|
+
if (mode === "nested" && stateKeysByContainer.has(container)) return false;
|
|
27
|
+
if (pass.inProgress.has(container)) return true;
|
|
28
|
+
if (pass.verdicts.has(container)) return pass.relaxable.has(container);
|
|
29
|
+
pass.inProgress.add(container);
|
|
30
|
+
const entries = walkDataEntries(container);
|
|
31
|
+
const keys = /* @__PURE__ */ new Set();
|
|
32
|
+
let dependedOnBackEdge = false;
|
|
33
|
+
pass.entriesByContainer.set(container, entries);
|
|
34
|
+
for (const entry of entries) {
|
|
35
|
+
const role = childRole(entry.value, entry.writable, mode);
|
|
36
|
+
if (role === "state") {
|
|
37
|
+
keys.add(entry.key);
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (role !== "descend") continue;
|
|
41
|
+
const child = entry.value;
|
|
42
|
+
if (typeof child !== "object" || child === null) continue;
|
|
43
|
+
if (visitContainer(child, pass, "nested")) dependedOnBackEdge = true;
|
|
44
|
+
if (readVerdict(child, pass).size > 0) keys.add(entry.key);
|
|
45
|
+
}
|
|
46
|
+
pass.inProgress.delete(container);
|
|
47
|
+
pass.verdicts.set(container, keys);
|
|
48
|
+
if (dependedOnBackEdge) pass.relaxable.add(container);
|
|
49
|
+
return dependedOnBackEdge;
|
|
50
|
+
}
|
|
51
|
+
function relaxVerdicts(pass, entryContainer) {
|
|
52
|
+
let gained = true;
|
|
53
|
+
while (gained) {
|
|
54
|
+
gained = false;
|
|
55
|
+
for (const container of pass.relaxable) {
|
|
56
|
+
const keys = pass.verdicts.get(container);
|
|
57
|
+
const entries = pass.entriesByContainer.get(container);
|
|
58
|
+
if (keys === void 0 || entries === void 0) continue;
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
if (keys.has(entry.key)) continue;
|
|
61
|
+
if (childRole(entry.value, entry.writable, container === entryContainer ? "entry" : "nested") !== "descend")
|
|
62
|
+
continue;
|
|
63
|
+
const child = entry.value;
|
|
64
|
+
if (typeof child !== "object" || child === null) continue;
|
|
65
|
+
if (readVerdict(child, pass).size === 0) continue;
|
|
66
|
+
keys.add(entry.key);
|
|
67
|
+
gained = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
var cacheNestedVerdicts = (pass, skip) => {
|
|
73
|
+
for (const [visited, keys] of pass.verdicts) {
|
|
74
|
+
if (visited === skip) continue;
|
|
75
|
+
stateKeysByContainer.set(visited, keys);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
function discoverStateKeys(container) {
|
|
79
|
+
const cached = stateKeysByContainer.get(container);
|
|
80
|
+
if (cached !== void 0) return cached;
|
|
81
|
+
if (isReactOwnNode(container)) return noStateKeys;
|
|
82
|
+
if (Object.isFrozen(container) || !canRebuild(container)) return noStateKeys;
|
|
83
|
+
const pass = createDiscoveryPass();
|
|
84
|
+
visitContainer(container, pass, "nested");
|
|
85
|
+
relaxVerdicts(pass);
|
|
86
|
+
cacheNestedVerdicts(pass);
|
|
87
|
+
return pass.verdicts.get(container) ?? noStateKeys;
|
|
88
|
+
}
|
|
89
|
+
function substituteContainer(container, substitution, wrap, mode) {
|
|
90
|
+
const rebuilt = substitution.rebuiltByContainer.get(container);
|
|
91
|
+
if (rebuilt !== void 0) return rebuilt;
|
|
92
|
+
let stateKeys;
|
|
93
|
+
if (mode === "nested") {
|
|
94
|
+
stateKeys = discoverStateKeys(container);
|
|
95
|
+
} else {
|
|
96
|
+
const pass = createDiscoveryPass();
|
|
97
|
+
visitContainer(container, pass, "entry");
|
|
98
|
+
relaxVerdicts(pass, container);
|
|
99
|
+
cacheNestedVerdicts(pass, container);
|
|
100
|
+
stateKeys = pass.verdicts.get(container) ?? noStateKeys;
|
|
101
|
+
}
|
|
102
|
+
if (stateKeys.size === 0) {
|
|
103
|
+
substitution.rebuiltByContainer.set(container, container);
|
|
104
|
+
return container;
|
|
105
|
+
}
|
|
106
|
+
const clone = Array.isArray(container) ? [] : {};
|
|
107
|
+
Reflect.setPrototypeOf(clone, Reflect.getPrototypeOf(container));
|
|
108
|
+
substitution.rebuiltByContainer.set(container, clone);
|
|
109
|
+
const descriptors = Object.getOwnPropertyDescriptors(container);
|
|
110
|
+
for (const key of stateKeys) {
|
|
111
|
+
const descriptor = descriptors[key];
|
|
112
|
+
if (descriptor === void 0 || !("value" in descriptor)) continue;
|
|
113
|
+
const value = descriptor.value;
|
|
114
|
+
const role = childRole(value, descriptor.writable === true, mode);
|
|
115
|
+
if (role === "state") {
|
|
116
|
+
const source = peelReadProxy(value);
|
|
117
|
+
if (typeof source !== "object" || source === null) continue;
|
|
118
|
+
if (!substitution.visitedSources.has(source)) {
|
|
119
|
+
substitution.visitedSources.add(source);
|
|
120
|
+
substitution.sources.push(source);
|
|
121
|
+
}
|
|
122
|
+
descriptors[key] = { ...descriptor, value: wrap(source) };
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
if (role !== "descend") continue;
|
|
126
|
+
if (typeof value !== "object" || value === null) continue;
|
|
127
|
+
descriptors[key] = { ...descriptor, value: substituteContainer(value, substitution, wrap, "nested") };
|
|
128
|
+
}
|
|
129
|
+
Object.defineProperties(clone, descriptors);
|
|
130
|
+
return clone;
|
|
131
|
+
}
|
|
132
|
+
function substituteStates(root, wrap) {
|
|
133
|
+
const substitution = {
|
|
134
|
+
rebuiltByContainer: /* @__PURE__ */ new Map(),
|
|
135
|
+
sources: [],
|
|
136
|
+
visitedSources: /* @__PURE__ */ new Set()
|
|
137
|
+
};
|
|
138
|
+
if (isReactOwnNode(root) || !canRebuild(root)) {
|
|
139
|
+
return { props: root, sources: substitution.sources };
|
|
140
|
+
}
|
|
141
|
+
return { props: substituteContainer(root, substitution, wrap, "entry"), sources: substitution.sources };
|
|
142
|
+
}
|
|
143
|
+
var NO_SLOT = /* @__PURE__ */ Symbol("opshot.noDispatcherSlot");
|
|
144
|
+
var readDispatcher = () => {
|
|
145
|
+
const internals = React;
|
|
146
|
+
const modern = internals.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;
|
|
147
|
+
if (modern !== void 0) return modern.H;
|
|
148
|
+
const legacy = internals.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED?.ReactCurrentDispatcher;
|
|
149
|
+
if (legacy !== void 0) return legacy.current;
|
|
150
|
+
return NO_SLOT;
|
|
151
|
+
};
|
|
152
|
+
var nonRenderDispatcher;
|
|
153
|
+
var learnNonRenderDispatcher = () => {
|
|
154
|
+
const current = readDispatcher();
|
|
155
|
+
if (current === NO_SLOT || current === null || current === void 0) return;
|
|
156
|
+
nonRenderDispatcher = current;
|
|
157
|
+
};
|
|
158
|
+
var isRendering = () => {
|
|
159
|
+
if (nonRenderDispatcher === void 0) return false;
|
|
160
|
+
const current = readDispatcher();
|
|
161
|
+
if (current === NO_SLOT || current === null || current === void 0) return false;
|
|
162
|
+
return current !== nonRenderDispatcher;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// src/react/readTracker.ts
|
|
166
|
+
var KEYS_PROPERTY = "k";
|
|
167
|
+
var HAS_KEY_PROPERTY = "h";
|
|
168
|
+
var HAS_OWN_KEY_PROPERTY = "o";
|
|
169
|
+
var ALL_OWN_KEYS_PROPERTY = "w";
|
|
170
|
+
var isWriteProxy = (value) => recordOf(value)?.proxy === value;
|
|
171
|
+
var getUsage = (affected, target) => {
|
|
172
|
+
let used = affected.get(target);
|
|
173
|
+
if (used === void 0) {
|
|
174
|
+
used = {};
|
|
175
|
+
affected.set(target, used);
|
|
176
|
+
}
|
|
177
|
+
return used;
|
|
178
|
+
};
|
|
179
|
+
var recordIn = (slot, key, value) => {
|
|
180
|
+
const map = slot ?? /* @__PURE__ */ new Map();
|
|
181
|
+
if (!map.has(key)) map.set(key, value);
|
|
182
|
+
return map;
|
|
183
|
+
};
|
|
184
|
+
var recordGet = (used, key, value) => {
|
|
185
|
+
used[KEYS_PROPERTY] = recordIn(used[KEYS_PROPERTY], key, value);
|
|
186
|
+
};
|
|
187
|
+
var recordHas = (used, key, value) => {
|
|
188
|
+
used[HAS_KEY_PROPERTY] = recordIn(used[HAS_KEY_PROPERTY], key, value);
|
|
189
|
+
};
|
|
190
|
+
var recordOwn = (used, key, value) => {
|
|
191
|
+
used[HAS_OWN_KEY_PROPERTY] = recordIn(used[HAS_OWN_KEY_PROPERTY], key, value);
|
|
192
|
+
};
|
|
193
|
+
var getPrototypeMethod = (target, prop) => {
|
|
194
|
+
let prototype = Reflect.getPrototypeOf(target);
|
|
195
|
+
while (prototype !== null) {
|
|
196
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(prototype, prop);
|
|
197
|
+
if (descriptor !== void 0) {
|
|
198
|
+
const descriptorValue = "value" in descriptor ? descriptor.value : void 0;
|
|
199
|
+
return typeof descriptorValue === "function" ? descriptorValue : void 0;
|
|
200
|
+
}
|
|
201
|
+
prototype = Reflect.getPrototypeOf(prototype);
|
|
202
|
+
}
|
|
203
|
+
return void 0;
|
|
204
|
+
};
|
|
205
|
+
var UnregisteredReadTrackerError = class extends Error {
|
|
206
|
+
constructor() {
|
|
207
|
+
super("opshot: readsIntersectDirty received an unregistered tracker");
|
|
208
|
+
this.name = "UnregisteredReadTrackerError";
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
var trackerPartitions = /* @__PURE__ */ new WeakMap();
|
|
212
|
+
var partitionsOf = (tracker) => {
|
|
213
|
+
const partitions = trackerPartitions.get(tracker);
|
|
214
|
+
if (partitions === void 0) throw new UnregisteredReadTrackerError();
|
|
215
|
+
return partitions;
|
|
216
|
+
};
|
|
217
|
+
var recordedKeysOf = (used) => {
|
|
218
|
+
const keyMaps = new Array();
|
|
219
|
+
if (used[KEYS_PROPERTY] !== void 0) keyMaps.push(used[KEYS_PROPERTY]);
|
|
220
|
+
if (used[HAS_KEY_PROPERTY] !== void 0) keyMaps.push(used[HAS_KEY_PROPERTY]);
|
|
221
|
+
if (used[HAS_OWN_KEY_PROPERTY] !== void 0) keyMaps.push(used[HAS_OWN_KEY_PROPERTY]);
|
|
222
|
+
return keyMaps;
|
|
223
|
+
};
|
|
224
|
+
var identityReadsChanged = (partition) => {
|
|
225
|
+
for (const [writeProxy, version] of partition.identityReads) {
|
|
226
|
+
if (partition.affected.has(writeProxy)) continue;
|
|
227
|
+
if (versionOf(rawOf(writeProxy)) !== version) return true;
|
|
228
|
+
}
|
|
229
|
+
return false;
|
|
230
|
+
};
|
|
231
|
+
function readsIntersectDirty(tracker, dirty) {
|
|
232
|
+
for (const partition of partitionsOf(tracker).values()) {
|
|
233
|
+
for (const [writeProxy, used] of partition.affected) {
|
|
234
|
+
const raw = rawOf(writeProxy);
|
|
235
|
+
const edges = dirty.edges.get(raw);
|
|
236
|
+
for (const keys of recordedKeysOf(used)) {
|
|
237
|
+
for (const key of keys.keys()) {
|
|
238
|
+
if (typeof key === "symbol") continue;
|
|
239
|
+
if (edges?.has(key) === true) return true;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (used[ALL_OWN_KEYS_PROPERTY] !== void 0 && dirty.nodes.has(raw)) return true;
|
|
243
|
+
}
|
|
244
|
+
if (identityReadsChanged(partition)) return true;
|
|
245
|
+
}
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
function readsChanged(tracker) {
|
|
249
|
+
for (const partition of partitionsOf(tracker).values()) {
|
|
250
|
+
for (const [writeProxy, used] of partition.affected) {
|
|
251
|
+
const gets = used[KEYS_PROPERTY];
|
|
252
|
+
if (gets !== void 0) {
|
|
253
|
+
for (const [key, stored] of gets) {
|
|
254
|
+
if (!Object.is(Reflect.get(writeProxy, key, writeProxy), stored)) return true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const hasKeys = used[HAS_KEY_PROPERTY];
|
|
258
|
+
if (hasKeys !== void 0) {
|
|
259
|
+
for (const [key, stored] of hasKeys) {
|
|
260
|
+
if (!Object.is(Reflect.has(writeProxy, key), stored)) return true;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const ownKeys = used[HAS_OWN_KEY_PROPERTY];
|
|
264
|
+
if (ownKeys !== void 0) {
|
|
265
|
+
for (const [key, stored] of ownKeys) {
|
|
266
|
+
const present = Reflect.getOwnPropertyDescriptor(writeProxy, key) !== void 0;
|
|
267
|
+
if (!Object.is(present, stored)) return true;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
const listed = used[ALL_OWN_KEYS_PROPERTY];
|
|
271
|
+
if (listed !== void 0) {
|
|
272
|
+
const current = Reflect.ownKeys(writeProxy);
|
|
273
|
+
if (current.length !== listed.length) return true;
|
|
274
|
+
for (let index = 0; index < listed.length; index += 1) {
|
|
275
|
+
if (!Object.is(current[index], listed[index])) return true;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (identityReadsChanged(partition)) return true;
|
|
280
|
+
}
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
function createReadTracker() {
|
|
284
|
+
const partitions = /* @__PURE__ */ new Map();
|
|
285
|
+
let afterRender = false;
|
|
286
|
+
let releasing = false;
|
|
287
|
+
const getPartition = (writeProxy) => {
|
|
288
|
+
let partition = partitions.get(writeProxy);
|
|
289
|
+
if (partition === void 0) {
|
|
290
|
+
partition = {
|
|
291
|
+
affected: /* @__PURE__ */ new Map(),
|
|
292
|
+
identityReads: /* @__PURE__ */ new Map(),
|
|
293
|
+
proxyCache: /* @__PURE__ */ new WeakMap()
|
|
294
|
+
};
|
|
295
|
+
partitions.set(writeProxy, partition);
|
|
296
|
+
}
|
|
297
|
+
return partition;
|
|
298
|
+
};
|
|
299
|
+
const shouldRecord = () => !afterRender || isRendering();
|
|
300
|
+
const trackUsage = (partition, writeProxy) => shouldRecord() ? getUsage(partition.affected, writeProxy) : {};
|
|
301
|
+
const recordIdentity = (partition, value) => {
|
|
302
|
+
if (!shouldRecord()) return;
|
|
303
|
+
if (isObjectLike(value) && isWriteProxy(value) && !partition.identityReads.has(value)) {
|
|
304
|
+
partition.identityReads.set(value, versionOf(rawOf(value)));
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
const toReadProxy = (writeProxy, partition) => {
|
|
308
|
+
const raw = rawOf(writeProxy);
|
|
309
|
+
const cached = partition.proxyCache.get(raw);
|
|
310
|
+
if (cached?.version === versionOf(raw)) return cached.proxy;
|
|
311
|
+
const target = raw;
|
|
312
|
+
const readProxyBox = {};
|
|
313
|
+
const boundMethods = /* @__PURE__ */ new WeakMap();
|
|
314
|
+
const bindMethodToReadProxy = (readProxy2, method) => {
|
|
315
|
+
const existing = boundMethods.get(method);
|
|
316
|
+
if (existing !== void 0) return existing;
|
|
317
|
+
const bound = Function.prototype.bind.call(method, readProxy2);
|
|
318
|
+
boundMethods.set(method, bound);
|
|
319
|
+
return bound;
|
|
320
|
+
};
|
|
321
|
+
const handler = {
|
|
322
|
+
get(_target, prop) {
|
|
323
|
+
const readProxy2 = readProxyBox.current;
|
|
324
|
+
const value = Reflect.get(writeProxy, prop, readProxy2 ?? writeProxy);
|
|
325
|
+
const used = trackUsage(partition, writeProxy);
|
|
326
|
+
recordGet(used, prop, value);
|
|
327
|
+
recordIdentity(partition, value);
|
|
328
|
+
if (typeof value === "function") {
|
|
329
|
+
const method = getPrototypeMethod(target, prop);
|
|
330
|
+
if (method !== void 0 && value === method && readProxy2 !== void 0) {
|
|
331
|
+
return bindMethodToReadProxy(readProxy2, method);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (!isObjectLike(value)) return value;
|
|
335
|
+
if (typeof value === "function") return value;
|
|
336
|
+
if (!isWriteProxy(value)) return value;
|
|
337
|
+
return toReadProxy(value, partition);
|
|
338
|
+
},
|
|
339
|
+
has(_target, prop) {
|
|
340
|
+
const used = trackUsage(partition, writeProxy);
|
|
341
|
+
const result = Reflect.has(writeProxy, prop);
|
|
342
|
+
recordHas(used, prop, result);
|
|
343
|
+
return result;
|
|
344
|
+
},
|
|
345
|
+
getOwnPropertyDescriptor(_target, prop) {
|
|
346
|
+
const used = trackUsage(partition, writeProxy);
|
|
347
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(writeProxy, prop);
|
|
348
|
+
recordOwn(used, prop, descriptor !== void 0);
|
|
349
|
+
return descriptor;
|
|
350
|
+
},
|
|
351
|
+
ownKeys() {
|
|
352
|
+
const used = trackUsage(partition, writeProxy);
|
|
353
|
+
const keys = Reflect.ownKeys(writeProxy);
|
|
354
|
+
used[ALL_OWN_KEYS_PROPERTY] ??= keys;
|
|
355
|
+
return keys;
|
|
356
|
+
},
|
|
357
|
+
set(_target, prop, value) {
|
|
358
|
+
return Reflect.set(writeProxy, prop, value, writeProxy);
|
|
359
|
+
},
|
|
360
|
+
deleteProperty(_target, prop) {
|
|
361
|
+
return Reflect.deleteProperty(writeProxy, prop);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
const readProxy = new Proxy(writeProxy, handler);
|
|
365
|
+
readProxyBox.current = readProxy;
|
|
366
|
+
registerReadProxyTarget(readProxy, writeProxy);
|
|
367
|
+
partition.proxyCache.set(raw, { proxy: readProxy, version: versionOf(raw) });
|
|
368
|
+
return readProxy;
|
|
369
|
+
};
|
|
370
|
+
const tracker = {
|
|
371
|
+
wrap(writeProxy) {
|
|
372
|
+
if (!isWriteProxy(writeProxy)) {
|
|
373
|
+
throw new Error("opshot: ReadTracker.wrap requires a write proxy");
|
|
374
|
+
}
|
|
375
|
+
const partition = getPartition(writeProxy);
|
|
376
|
+
return toReadProxy(writeProxy, partition);
|
|
377
|
+
},
|
|
378
|
+
captureReads() {
|
|
379
|
+
learnNonRenderDispatcher();
|
|
380
|
+
afterRender = true;
|
|
381
|
+
},
|
|
382
|
+
resetReads() {
|
|
383
|
+
afterRender = false;
|
|
384
|
+
for (const partition of partitions.values()) {
|
|
385
|
+
partition.affected.clear();
|
|
386
|
+
partition.identityReads.clear();
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
retain() {
|
|
390
|
+
releasing = false;
|
|
391
|
+
},
|
|
392
|
+
dispose() {
|
|
393
|
+
releasing = true;
|
|
394
|
+
void Promise.resolve().then(() => {
|
|
395
|
+
if (!releasing) return;
|
|
396
|
+
releasing = false;
|
|
397
|
+
partitions.clear();
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
};
|
|
401
|
+
trackerPartitions.set(tracker, partitions);
|
|
402
|
+
return tracker;
|
|
403
|
+
}
|
|
404
|
+
var useCommitEffect = typeof document === "undefined" ? useEffect : useLayoutEffect;
|
|
405
|
+
|
|
406
|
+
// src/react/scope.tsx
|
|
407
|
+
var sourcesKey = (sources) => `${sources.length}:${sources.map((source) => addressOf(source)).join(",")}`;
|
|
408
|
+
var uniqueHandlesOf = (nodes) => {
|
|
409
|
+
const unique = new Array();
|
|
410
|
+
const seen = /* @__PURE__ */ new Set();
|
|
411
|
+
for (const node of nodes) {
|
|
412
|
+
for (const handle of handlesOf(node)) {
|
|
413
|
+
if (seen.has(handle)) continue;
|
|
414
|
+
seen.add(handle);
|
|
415
|
+
unique.push(handle);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
return unique;
|
|
419
|
+
};
|
|
420
|
+
var arePropsEqual = (previous, next) => {
|
|
421
|
+
const previousRecord = previous;
|
|
422
|
+
const nextRecord = next;
|
|
423
|
+
const keys = Object.keys(previousRecord);
|
|
424
|
+
if (keys.length !== Object.keys(nextRecord).length) return false;
|
|
425
|
+
for (const key of keys) {
|
|
426
|
+
const before = previousRecord[key];
|
|
427
|
+
const after = nextRecord[key];
|
|
428
|
+
if (Object.is(before, after)) continue;
|
|
429
|
+
if (typeof before === "object" && before !== null && typeof after === "object" && after !== null && isState(before) && isState(after) && isSameIdentity(before, after)) {
|
|
430
|
+
continue;
|
|
431
|
+
}
|
|
432
|
+
return false;
|
|
433
|
+
}
|
|
434
|
+
return true;
|
|
435
|
+
};
|
|
436
|
+
function scope(Component) {
|
|
437
|
+
const Scoped = (props) => {
|
|
438
|
+
const readTrackerRef = useRef(void 0);
|
|
439
|
+
const currentHandlesRef = useRef([]);
|
|
440
|
+
readTrackerRef.current ??= createReadTracker();
|
|
441
|
+
const readTracker = readTrackerRef.current;
|
|
442
|
+
const [, bump] = useReducer((value) => value + 1, 0);
|
|
443
|
+
readTracker.resetReads();
|
|
444
|
+
const { props: renderedProps, sources } = substituteStates(props, (source) => readTracker.wrap(source));
|
|
445
|
+
const uniqueHandles = uniqueHandlesOf(sources);
|
|
446
|
+
useCommitEffect(() => {
|
|
447
|
+
currentHandlesRef.current = uniqueHandles;
|
|
448
|
+
readTracker.captureReads();
|
|
449
|
+
});
|
|
450
|
+
useEffect(() => {
|
|
451
|
+
readTracker.retain();
|
|
452
|
+
return () => readTracker.dispose();
|
|
453
|
+
}, [readTracker]);
|
|
454
|
+
useEffect(() => {
|
|
455
|
+
let cancelled = false;
|
|
456
|
+
const subscribedHandles = uniqueHandlesOf(sources);
|
|
457
|
+
const unsubscribes = subscribedHandles.map(
|
|
458
|
+
(handle) => subscribe(proxyOf(handle.root), () => {
|
|
459
|
+
if (cancelled) return;
|
|
460
|
+
const dirty = handle.lastDirty;
|
|
461
|
+
if (dirty !== void 0 && readsIntersectDirty(readTracker, dirty)) bump();
|
|
462
|
+
})
|
|
463
|
+
);
|
|
464
|
+
if (readsChanged(readTracker)) bump();
|
|
465
|
+
return () => {
|
|
466
|
+
cancelled = true;
|
|
467
|
+
for (const unsubscribe of unsubscribes) unsubscribe();
|
|
468
|
+
};
|
|
469
|
+
}, [sourcesKey(sources), readTracker]);
|
|
470
|
+
return createElement(Component, renderedProps);
|
|
471
|
+
};
|
|
472
|
+
const baseName = Component.displayName ?? Component.name;
|
|
473
|
+
Scoped.displayName = `scope(${typeof baseName === "string" && baseName !== "" ? baseName : "Component"})`;
|
|
474
|
+
return memo(Scoped, arePropsEqual);
|
|
475
|
+
}
|
|
476
|
+
var isObjectLike2 = (value) => value !== null && (typeof value === "object" || typeof value === "function");
|
|
477
|
+
function useMutableState(properties, options) {
|
|
478
|
+
const [{ writeProxy, readTracker }] = useState(() => {
|
|
479
|
+
const initial = typeof properties === "function" ? properties() : properties;
|
|
480
|
+
return {
|
|
481
|
+
writeProxy: createMutableState(initial, options),
|
|
482
|
+
readTracker: createReadTracker()
|
|
483
|
+
};
|
|
484
|
+
});
|
|
485
|
+
const [, bump] = useReducer((value) => value + 1, 0);
|
|
486
|
+
const currentHandlesRef = useRef([]);
|
|
487
|
+
const uniqueHandles = isObjectLike2(writeProxy) ? handlesOf(writeProxy) : [];
|
|
488
|
+
readTracker.resetReads();
|
|
489
|
+
const readProxy = isObjectLike2(writeProxy) ? readTracker.wrap(writeProxy) : writeProxy;
|
|
490
|
+
useCommitEffect(() => {
|
|
491
|
+
currentHandlesRef.current = uniqueHandles;
|
|
492
|
+
readTracker.captureReads();
|
|
493
|
+
});
|
|
494
|
+
useEffect(() => {
|
|
495
|
+
readTracker.retain();
|
|
496
|
+
return () => readTracker.dispose();
|
|
497
|
+
}, [readTracker]);
|
|
498
|
+
useEffect(() => {
|
|
499
|
+
let cancelled = false;
|
|
500
|
+
const subscribedHandles = isObjectLike2(writeProxy) ? handlesOf(writeProxy) : [];
|
|
501
|
+
const unsubscribes = subscribedHandles.map(
|
|
502
|
+
(handle) => subscribe(proxyOf(handle.root), () => {
|
|
503
|
+
if (cancelled) return;
|
|
504
|
+
const dirty = handle.lastDirty;
|
|
505
|
+
if (dirty !== void 0 && readsIntersectDirty(readTracker, dirty)) bump();
|
|
506
|
+
})
|
|
507
|
+
);
|
|
508
|
+
if (readsChanged(readTracker)) bump();
|
|
509
|
+
return () => {
|
|
510
|
+
cancelled = true;
|
|
511
|
+
for (const unsubscribe of unsubscribes) unsubscribe();
|
|
512
|
+
};
|
|
513
|
+
}, [writeProxy, readTracker]);
|
|
514
|
+
return readProxy;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export { scope, useMutableState };
|
package/package.json
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opshot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Plain-object state for React: mutate it directly, re-render only the components that read what changed, and track every change operation",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
8
8
|
"import": "./dist/index.js",
|
|
9
9
|
"types": "./dist/index.d.ts"
|
|
10
|
+
},
|
|
11
|
+
"./react": {
|
|
12
|
+
"import": "./dist/react.js",
|
|
13
|
+
"types": "./dist/react.d.ts"
|
|
10
14
|
}
|
|
11
15
|
},
|
|
12
16
|
"files": [
|
|
@@ -36,6 +40,11 @@
|
|
|
36
40
|
"peerDependencies": {
|
|
37
41
|
"react": ">=16.8.0"
|
|
38
42
|
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"react": {
|
|
45
|
+
"optional": true
|
|
46
|
+
}
|
|
47
|
+
},
|
|
39
48
|
"devDependencies": {
|
|
40
49
|
"@commitlint/cli": "^19.8.1",
|
|
41
50
|
"@commitlint/config-conventional": "^19.8.1",
|