on-zero 0.11.2 → 0.11.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/useStreamingField.cjs +19 -28
- package/dist/cjs/useStreamingField.loop.test.cjs +101 -0
- package/dist/cjs/useStreamingField.loop.test.native.js +115 -0
- package/dist/cjs/useStreamingField.loop.test.native.js.map +1 -0
- package/dist/cjs/useStreamingField.native.js +19 -33
- package/dist/cjs/useStreamingField.native.js.map +1 -1
- package/dist/esm/useStreamingField.loop.test.mjs +102 -0
- package/dist/esm/useStreamingField.loop.test.mjs.map +1 -0
- package/dist/esm/useStreamingField.loop.test.native.js +113 -0
- package/dist/esm/useStreamingField.loop.test.native.js.map +1 -0
- package/dist/esm/useStreamingField.mjs +20 -29
- package/dist/esm/useStreamingField.mjs.map +1 -1
- package/dist/esm/useStreamingField.native.js +20 -34
- package/dist/esm/useStreamingField.native.js.map +1 -1
- package/package.json +2 -3
- package/src/useStreamingField.loop.test.tsx +111 -0
- package/src/useStreamingField.ts +43 -33
- package/types/useStreamingField.d.ts +1 -1
- package/types/useStreamingField.d.ts.map +1 -1
- package/types/useStreamingField.loop.test.d.ts +5 -0
- package/types/useStreamingField.loop.test.d.ts.map +1 -0
|
@@ -26,7 +26,7 @@ __export(useStreamingField_exports, {
|
|
|
26
26
|
createUseStreamingFields: () => createUseStreamingFields
|
|
27
27
|
});
|
|
28
28
|
module.exports = __toCommonJS(useStreamingField_exports);
|
|
29
|
-
var import_realtime = require("orez-
|
|
29
|
+
var import_realtime = require("orez-lite/realtime");
|
|
30
30
|
var import_react = require("react");
|
|
31
31
|
function createUseStreamingField(getStore) {
|
|
32
32
|
return function useStreamingField(handle, base) {
|
|
@@ -36,9 +36,13 @@ function createUseStreamingField(getStore) {
|
|
|
36
36
|
if (!store || !handle) return () => {};
|
|
37
37
|
return store.subscribe(handle, onChange);
|
|
38
38
|
}, [store, id]);
|
|
39
|
+
const last = (0, import_react.useRef)(void 0);
|
|
39
40
|
const getSnapshot = (0, import_react.useCallback)(() => {
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
const next = !store || !handle ? durableState(base) : store.read(handle, base);
|
|
42
|
+
const cached = last.current;
|
|
43
|
+
if (cached && sameState(cached, next)) return cached;
|
|
44
|
+
last.current = next;
|
|
45
|
+
return next;
|
|
42
46
|
}, [store, id, base]);
|
|
43
47
|
return (0, import_react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
44
48
|
};
|
|
@@ -58,15 +62,18 @@ function createUseStreamingFields(getStore) {
|
|
|
58
62
|
value: {}
|
|
59
63
|
}), [store, topicsKey]);
|
|
60
64
|
const getSnapshot = (0, import_react.useCallback)(() => {
|
|
65
|
+
const previous = cache.value;
|
|
61
66
|
const next = {};
|
|
62
67
|
let changed = false;
|
|
63
68
|
for (const request of requests) {
|
|
64
|
-
const
|
|
69
|
+
const raw = store ? store.read(request.handle, request.base) : durableState(request.base);
|
|
70
|
+
const cached = previous[request.key];
|
|
71
|
+
const state = cached && sameState(cached, raw) ? cached : raw;
|
|
65
72
|
next[request.key] = state;
|
|
66
|
-
if (
|
|
73
|
+
if (state !== cached) changed = true;
|
|
67
74
|
}
|
|
68
|
-
if (!changed && Object.keys(
|
|
69
|
-
return
|
|
75
|
+
if (!changed && Object.keys(previous).length === Object.keys(next).length) {
|
|
76
|
+
return previous;
|
|
70
77
|
}
|
|
71
78
|
cache.value = next;
|
|
72
79
|
return next;
|
|
@@ -74,29 +81,13 @@ function createUseStreamingFields(getStore) {
|
|
|
74
81
|
return (0, import_react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
75
82
|
};
|
|
76
83
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
const cached = DURABLE_CACHE.get(base);
|
|
83
|
-
if (cached) return cached;
|
|
84
|
-
const state = {
|
|
85
|
-
value: base,
|
|
86
|
-
phase: "durable",
|
|
87
|
-
streamID: null
|
|
88
|
-
};
|
|
89
|
-
DURABLE_CACHE.set(base, state);
|
|
90
|
-
return state;
|
|
91
|
-
}
|
|
92
|
-
if (lastPrimitiveState && Object.is(lastPrimitiveBase, base)) {
|
|
93
|
-
return lastPrimitiveState;
|
|
94
|
-
}
|
|
95
|
-
lastPrimitiveBase = base;
|
|
96
|
-
lastPrimitiveState = {
|
|
84
|
+
function sameState(a, b) {
|
|
85
|
+
return Object.is(a.value, b.value) && a.phase === b.phase && a.streamID === b.streamID;
|
|
86
|
+
}
|
|
87
|
+
function durableState(base) {
|
|
88
|
+
return {
|
|
97
89
|
value: base,
|
|
98
90
|
phase: "durable",
|
|
99
91
|
streamID: null
|
|
100
92
|
};
|
|
101
|
-
return lastPrimitiveState;
|
|
102
93
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
var import_zero = require("@rocicorp/zero");
|
|
2
|
+
var import_realtime = require("orez-lite/realtime");
|
|
3
|
+
var import_react = require("react");
|
|
4
|
+
var import_client = require("react-dom/client");
|
|
5
|
+
var import_vitest = require("vitest");
|
|
6
|
+
var import_useStreamingField = require("./useStreamingField.cjs");
|
|
7
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
8
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
9
|
+
const sootTask = (0, import_zero.table)("sootTask").columns({
|
|
10
|
+
id: (0, import_zero.string)(),
|
|
11
|
+
title: (0, import_zero.string)(),
|
|
12
|
+
description: (0, import_zero.string)()
|
|
13
|
+
}).primaryKey("id");
|
|
14
|
+
const schema = (0, import_zero.createSchema)({
|
|
15
|
+
tables: [sootTask]
|
|
16
|
+
});
|
|
17
|
+
const streaming = (0, import_realtime.defineStreamingFields)(schema, {
|
|
18
|
+
sootTask: {
|
|
19
|
+
description: {
|
|
20
|
+
maxBytes: 1e5,
|
|
21
|
+
maxUpdatesPerSecond: 30,
|
|
22
|
+
maxBytesPerSecond: 2e5
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
let container;
|
|
27
|
+
let root;
|
|
28
|
+
let consoleError;
|
|
29
|
+
(0, import_vitest.beforeEach)(() => {
|
|
30
|
+
container = document.createElement("div");
|
|
31
|
+
document.body.appendChild(container);
|
|
32
|
+
root = (0, import_client.createRoot)(container);
|
|
33
|
+
consoleError = import_vitest.vi.spyOn(console, "error").mockImplementation(() => {});
|
|
34
|
+
});
|
|
35
|
+
(0, import_vitest.afterEach)(() => {
|
|
36
|
+
(0, import_react.act)(() => root.unmount());
|
|
37
|
+
container.remove();
|
|
38
|
+
consoleError.mockRestore();
|
|
39
|
+
});
|
|
40
|
+
const uncachedSnapshotWarnings = () => consoleError.mock.calls.filter(call => String(call[0]).includes("getSnapshot should be cached"));
|
|
41
|
+
(0, import_vitest.test)("single-field hook is snapshot-stable while its topic is unsubscribed", () => {
|
|
42
|
+
const realtime = (0, import_realtime.createLocalRealtime)({
|
|
43
|
+
manifest: streaming.manifest
|
|
44
|
+
});
|
|
45
|
+
const useStreamingField = (0, import_useStreamingField.createUseStreamingField)(() => realtime.store);
|
|
46
|
+
function Card({
|
|
47
|
+
id,
|
|
48
|
+
base
|
|
49
|
+
}) {
|
|
50
|
+
const state = useStreamingField(streaming.sootTask.description({
|
|
51
|
+
id
|
|
52
|
+
}), base);
|
|
53
|
+
return /* @__PURE__ */(0, import_jsx_runtime.jsx)("div", {
|
|
54
|
+
children: state.value
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
(0, import_react.act)(() => {
|
|
58
|
+
root.render(/* @__PURE__ */(0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, {
|
|
59
|
+
children: [/* @__PURE__ */(0, import_jsx_runtime.jsx)(Card, {
|
|
60
|
+
id: "t1",
|
|
61
|
+
base: ""
|
|
62
|
+
}), /* @__PURE__ */(0, import_jsx_runtime.jsx)(Card, {
|
|
63
|
+
id: "t2",
|
|
64
|
+
base: "a durable description"
|
|
65
|
+
}), /* @__PURE__ */(0, import_jsx_runtime.jsx)(Card, {
|
|
66
|
+
id: "t3",
|
|
67
|
+
base: ""
|
|
68
|
+
})]
|
|
69
|
+
}));
|
|
70
|
+
});
|
|
71
|
+
(0, import_vitest.expect)(uncachedSnapshotWarnings()).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
(0, import_vitest.test)("multi-field hook is snapshot-stable while topics are unsubscribed", () => {
|
|
74
|
+
const realtime = (0, import_realtime.createLocalRealtime)({
|
|
75
|
+
manifest: streaming.manifest
|
|
76
|
+
});
|
|
77
|
+
const useStreamingFields = (0, import_useStreamingField.createUseStreamingFields)(() => realtime.store);
|
|
78
|
+
const requests = [{
|
|
79
|
+
key: "t1",
|
|
80
|
+
handle: streaming.sootTask.description({
|
|
81
|
+
id: "t1"
|
|
82
|
+
}),
|
|
83
|
+
base: ""
|
|
84
|
+
}, {
|
|
85
|
+
key: "t2",
|
|
86
|
+
handle: streaming.sootTask.description({
|
|
87
|
+
id: "t2"
|
|
88
|
+
}),
|
|
89
|
+
base: "x"
|
|
90
|
+
}];
|
|
91
|
+
function List() {
|
|
92
|
+
const states = useStreamingFields(requests);
|
|
93
|
+
return /* @__PURE__ */(0, import_jsx_runtime.jsx)("div", {
|
|
94
|
+
children: Object.keys(states).length
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
(0, import_react.act)(() => {
|
|
98
|
+
root.render(/* @__PURE__ */(0, import_jsx_runtime.jsx)(List, {}));
|
|
99
|
+
});
|
|
100
|
+
(0, import_vitest.expect)(uncachedSnapshotWarnings()).toEqual([]);
|
|
101
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
4
|
+
var import_zero = require("@rocicorp/zero");
|
|
5
|
+
var import_realtime = require("orez-lite/realtime");
|
|
6
|
+
var import_react = require("react");
|
|
7
|
+
var import_client = require("react-dom/client");
|
|
8
|
+
var import_vitest = require("vitest");
|
|
9
|
+
var import_useStreamingField = require("./useStreamingField.native.js");
|
|
10
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
11
|
+
var sootTask = (0, import_zero.table)("sootTask").columns({
|
|
12
|
+
id: (0, import_zero.string)(),
|
|
13
|
+
title: (0, import_zero.string)(),
|
|
14
|
+
description: (0, import_zero.string)()
|
|
15
|
+
}).primaryKey("id");
|
|
16
|
+
var schema = (0, import_zero.createSchema)({
|
|
17
|
+
tables: [sootTask]
|
|
18
|
+
});
|
|
19
|
+
var streaming = (0, import_realtime.defineStreamingFields)(schema, {
|
|
20
|
+
sootTask: {
|
|
21
|
+
description: {
|
|
22
|
+
maxBytes: 1e5,
|
|
23
|
+
maxUpdatesPerSecond: 30,
|
|
24
|
+
maxBytesPerSecond: 2e5
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
var container;
|
|
29
|
+
var root;
|
|
30
|
+
var consoleError;
|
|
31
|
+
(0, import_vitest.beforeEach)(function () {
|
|
32
|
+
container = document.createElement("div");
|
|
33
|
+
document.body.appendChild(container);
|
|
34
|
+
root = (0, import_client.createRoot)(container);
|
|
35
|
+
consoleError = import_vitest.vi.spyOn(console, "error").mockImplementation(function () {});
|
|
36
|
+
});
|
|
37
|
+
(0, import_vitest.afterEach)(function () {
|
|
38
|
+
(0, import_react.act)(function () {
|
|
39
|
+
return root.unmount();
|
|
40
|
+
});
|
|
41
|
+
container.remove();
|
|
42
|
+
consoleError.mockRestore();
|
|
43
|
+
});
|
|
44
|
+
var uncachedSnapshotWarnings = function uncachedSnapshotWarnings2() {
|
|
45
|
+
return consoleError.mock.calls.filter(function (call) {
|
|
46
|
+
return String(call[0]).includes("getSnapshot should be cached");
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
(0, import_vitest.test)("single-field hook is snapshot-stable while its topic is unsubscribed", function () {
|
|
50
|
+
var realtime = (0, import_realtime.createLocalRealtime)({
|
|
51
|
+
manifest: streaming.manifest
|
|
52
|
+
});
|
|
53
|
+
var useStreamingField = (0, import_useStreamingField.createUseStreamingField)(function () {
|
|
54
|
+
return realtime.store;
|
|
55
|
+
});
|
|
56
|
+
function Card(param) {
|
|
57
|
+
var {
|
|
58
|
+
id,
|
|
59
|
+
base
|
|
60
|
+
} = param;
|
|
61
|
+
var state = useStreamingField(streaming.sootTask.description({
|
|
62
|
+
id
|
|
63
|
+
}), base);
|
|
64
|
+
return /* @__PURE__ */(0, import_jsx_runtime.jsx)("div", {
|
|
65
|
+
children: state.value
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
(0, import_react.act)(function () {
|
|
69
|
+
root.render(/* @__PURE__ */(0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, {
|
|
70
|
+
children: [/* @__PURE__ */(0, import_jsx_runtime.jsx)(Card, {
|
|
71
|
+
id: "t1",
|
|
72
|
+
base: ""
|
|
73
|
+
}), /* @__PURE__ */(0, import_jsx_runtime.jsx)(Card, {
|
|
74
|
+
id: "t2",
|
|
75
|
+
base: "a durable description"
|
|
76
|
+
}), /* @__PURE__ */(0, import_jsx_runtime.jsx)(Card, {
|
|
77
|
+
id: "t3",
|
|
78
|
+
base: ""
|
|
79
|
+
})]
|
|
80
|
+
}));
|
|
81
|
+
});
|
|
82
|
+
(0, import_vitest.expect)(uncachedSnapshotWarnings()).toEqual([]);
|
|
83
|
+
});
|
|
84
|
+
(0, import_vitest.test)("multi-field hook is snapshot-stable while topics are unsubscribed", function () {
|
|
85
|
+
var realtime = (0, import_realtime.createLocalRealtime)({
|
|
86
|
+
manifest: streaming.manifest
|
|
87
|
+
});
|
|
88
|
+
var useStreamingFields = (0, import_useStreamingField.createUseStreamingFields)(function () {
|
|
89
|
+
return realtime.store;
|
|
90
|
+
});
|
|
91
|
+
var requests = [{
|
|
92
|
+
key: "t1",
|
|
93
|
+
handle: streaming.sootTask.description({
|
|
94
|
+
id: "t1"
|
|
95
|
+
}),
|
|
96
|
+
base: ""
|
|
97
|
+
}, {
|
|
98
|
+
key: "t2",
|
|
99
|
+
handle: streaming.sootTask.description({
|
|
100
|
+
id: "t2"
|
|
101
|
+
}),
|
|
102
|
+
base: "x"
|
|
103
|
+
}];
|
|
104
|
+
function List() {
|
|
105
|
+
var states = useStreamingFields(requests);
|
|
106
|
+
return /* @__PURE__ */(0, import_jsx_runtime.jsx)("div", {
|
|
107
|
+
children: Object.keys(states).length
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
(0, import_react.act)(function () {
|
|
111
|
+
root.render(/* @__PURE__ */(0, import_jsx_runtime.jsx)(List, {}));
|
|
112
|
+
});
|
|
113
|
+
(0, import_vitest.expect)(uncachedSnapshotWarnings()).toEqual([]);
|
|
114
|
+
});
|
|
115
|
+
//# sourceMappingURL=useStreamingField.loop.test.native.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["import_jsx_runtime","require","import_zero","import_realtime","import_react","import_client","import_vitest","import_useStreamingField","globalThis","IS_REACT_ACT_ENVIRONMENT","sootTask","table","columns","id","string","title","description","primaryKey","schema","createSchema","tables","streaming","defineStreamingFields","maxBytes","maxUpdatesPerSecond","maxBytesPerSecond","container","root","consoleError","beforeEach","document","createElement","body","appendChild","createRoot","vi","spyOn","console","mockImplementation","afterEach","act","unmount","remove","mockRestore","uncachedSnapshotWarnings","uncachedSnapshotWarnings2","mock","calls","filter","call","String","includes","test","realtime","createLocalRealtime","manifest","useStreamingField","createUseStreamingField","store","Card","param","base","state","jsx","children","value","render","jsxs","Fragment"],"sources":["../../src/useStreamingField.loop.test.tsx"],"sourcesContent":[null],"mappings":"AAeA;;AACA,IAAAA,kBAA2D,GAAAC,OAAA;AAC3D,IAAAC,WAAA,GAAAD,OAAoB;AACpB,IAAAE,eAAA,GAAsCF,OAAA;AACtC,IAAAG,YAAA,GAAAH,OAAwD;AAExD,IAAAI,aAAA,GAAAJ,OAAA,mBAAkE;AAmDvD,IAAAK,aAAA,GAAAL,OAAA;AA7CX,IAAAM,wBAAW,GAAAN,OAA2B;AAEtCO,UAAM,CAAAC,wBAAW,OAAM;AAGvB,IAAAC,QAAM,OAASR,WAAA,CAAAS,KAAA,YAAe,EAAAC,OAAS;EAEvCC,EAAA,EAAM,IAAAX,WAAA,CAAYY,MAAA;EAChBC,KAAA,MAAUb,WAAA,CAAAY,MAAA;EAAAE,WACR,IAAa,EAAAd,WAAA,CAAAY,MAAA;AAAA,GAAAG,UACX,KAAU;AAAA,IAAAC,MACV,OAAAhB,WAAqB,CAAAiB,YAAA;EAAAC,MACrB,GACFV,QAAA;AAEJ,CAAC;AAED,IAAIW,SAAA,OAAAlB,eAAA,CAAAmB,qBAAA,EAAAJ,MAAA;EACJR,QAAI;IACAM,WAAA;MAEJO,QAAA;MACEC,mBAAqB;MACrBC,iBAAc;IACd;EACA;AAAmE,EAAC;AACtE,IAACC,SAAA;AAAA,IAEDC,IAAA;AACE,IAAAC,YAAA;AACA,IAAAtB,aAAU,CAAAuB,UAAO;EACjBH,SAAA,GAAAI,QAAa,CAAAC,aAAY;EAC1BD,QAAA,CAAAE,IAAA,CAAAC,WAAA,CAAAP,SAAA;EAEDC,IAAM,OAAAtB,aAAA,CAAA6B,UAA2B,EAC/BR,SAAA;EAA+BE,YAC7B,GAAAtB,aAAgB,CAAA6B,EAAA,CAAAC,KAAS,CAAAC,OAAA,WAAAC,kBAA8B,cACzD;AAAA;AAGA,IAAAhC,aAAM,CAAAiC,SAAW;EACjB,IAAAnC,YAAM,CAAAoC,GAAA,cAAoB;IAE1B,OAASb,IAAA,CAAKc,OAAM;EAClB;EACAf,SAAO,CAAAgB,MAAA;EACTd,YAAA,CAAAe,WAAA;AAEA;AAIE,IAAAC,wBAAK,YAAAC,0BAAA;EAAA,OACHjB,YAAA,CAAAkB,IAAA,CAAAC,KAAA,CAAAC,MAAA,WAAAC,IAAA;IACE,OAAAC,MAAA,CAAAD,IAAA,KAAAE,QAAA,+BAAS;EAAa;AACqB;AACrB,IAAA7C,aACxB,CAAA8C,IAAA;EAAA,IACFC,QAAA,OAAAlD,eAAA,CAAAmD,mBAAA;IACDC,QAAA,EAAAlC,SAAA,CAAAkC;EAED;EACD,IAAAC,iBAAA,OAAAjD,wBAAA,CAAAkD,uBAAA;IAED,OAAAJ,QAAA,CAAAK,KAAK;EACH;EACA,SAAMC,KAAAC,KAAA;IAEN,IAAM;MAAA/C,EAAA;MAAAgD;IAAW,IAAAD,KAAA;IACf,IAAEE,KAAK,GAAAN,iBAAc,CAAAnC,SAAU,CAASX,QAAA,CAAAM,WAAkB;MACxDH;IACJ,IAAAgD,IAAA;IAEA,OAAS,eAAO,IAAA7D,kBAAA,CAAA+D,GAAA;MACdC,QAAM,EAAAF,KAAS,CAAAG;IACf;EACF;EAEA,IAAA7D,YAAA,CAAAoC,GAAA,EAAI,YAAM;IACRb,IAAA,CAAKuC,MAAA,CAAO,mBAAAlE,kBAAA,CAAAmE,IAAA,EAACnE,kBAAO,CAAAoE,QAAA;MACrBJ,QAAA,GAED,mBAAOhE,kBAAA,CAAA+D,GAAA,EAAAJ,IAA4B;QACpC9C,EAAA","ignoreList":[]}
|
|
@@ -28,13 +28,8 @@ __export(useStreamingField_exports, {
|
|
|
28
28
|
createUseStreamingFields: () => createUseStreamingFields
|
|
29
29
|
});
|
|
30
30
|
module.exports = __toCommonJS(useStreamingField_exports);
|
|
31
|
-
var import_realtime = require("orez-
|
|
31
|
+
var import_realtime = require("orez-lite/realtime");
|
|
32
32
|
var import_react = require("react");
|
|
33
|
-
function _type_of(obj) {
|
|
34
|
-
"@swc/helpers - typeof";
|
|
35
|
-
|
|
36
|
-
return obj && typeof Symbol !== "undefined" && obj.constructor === Symbol ? "symbol" : typeof obj;
|
|
37
|
-
}
|
|
38
33
|
function createUseStreamingField(getStore) {
|
|
39
34
|
return function useStreamingField(handle, base) {
|
|
40
35
|
var store = getStore();
|
|
@@ -43,9 +38,13 @@ function createUseStreamingField(getStore) {
|
|
|
43
38
|
if (!store || !handle) return function () {};
|
|
44
39
|
return store.subscribe(handle, onChange);
|
|
45
40
|
}, [store, id]);
|
|
41
|
+
var last = (0, import_react.useRef)(void 0);
|
|
46
42
|
var getSnapshot = (0, import_react.useCallback)(function () {
|
|
47
|
-
|
|
48
|
-
|
|
43
|
+
var next = !store || !handle ? durableState(base) : store.read(handle, base);
|
|
44
|
+
var cached = last.current;
|
|
45
|
+
if (cached && sameState(cached, next)) return cached;
|
|
46
|
+
last.current = next;
|
|
47
|
+
return next;
|
|
49
48
|
}, [store, id, base]);
|
|
50
49
|
return (0, import_react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
51
50
|
};
|
|
@@ -94,6 +93,7 @@ function createUseStreamingFields(getStore) {
|
|
|
94
93
|
};
|
|
95
94
|
}, [store, topicsKey]);
|
|
96
95
|
var getSnapshot = (0, import_react.useCallback)(function () {
|
|
96
|
+
var previous = cache.value;
|
|
97
97
|
var next = {};
|
|
98
98
|
var changed = false;
|
|
99
99
|
var _iteratorNormalCompletion = true,
|
|
@@ -102,9 +102,11 @@ function createUseStreamingFields(getStore) {
|
|
|
102
102
|
try {
|
|
103
103
|
for (var _iterator = requests[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
|
|
104
104
|
var request = _step.value;
|
|
105
|
-
var
|
|
105
|
+
var raw = store ? store.read(request.handle, request.base) : durableState(request.base);
|
|
106
|
+
var cached = previous[request.key];
|
|
107
|
+
var state = cached && sameState(cached, raw) ? cached : raw;
|
|
106
108
|
next[request.key] = state;
|
|
107
|
-
if (
|
|
109
|
+
if (state !== cached) changed = true;
|
|
108
110
|
}
|
|
109
111
|
} catch (err) {
|
|
110
112
|
_didIteratorError = true;
|
|
@@ -120,8 +122,8 @@ function createUseStreamingFields(getStore) {
|
|
|
120
122
|
}
|
|
121
123
|
}
|
|
122
124
|
}
|
|
123
|
-
if (!changed && Object.keys(
|
|
124
|
-
return
|
|
125
|
+
if (!changed && Object.keys(previous).length === Object.keys(next).length) {
|
|
126
|
+
return previous;
|
|
125
127
|
}
|
|
126
128
|
cache.value = next;
|
|
127
129
|
return next;
|
|
@@ -129,30 +131,14 @@ function createUseStreamingFields(getStore) {
|
|
|
129
131
|
return (0, import_react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
|
|
130
132
|
};
|
|
131
133
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
function
|
|
136
|
-
|
|
137
|
-
var cached = DURABLE_CACHE.get(base);
|
|
138
|
-
if (cached) return cached;
|
|
139
|
-
var state = {
|
|
140
|
-
value: base,
|
|
141
|
-
phase: "durable",
|
|
142
|
-
streamID: null
|
|
143
|
-
};
|
|
144
|
-
DURABLE_CACHE.set(base, state);
|
|
145
|
-
return state;
|
|
146
|
-
}
|
|
147
|
-
if (lastPrimitiveState && Object.is(lastPrimitiveBase, base)) {
|
|
148
|
-
return lastPrimitiveState;
|
|
149
|
-
}
|
|
150
|
-
lastPrimitiveBase = base;
|
|
151
|
-
lastPrimitiveState = {
|
|
134
|
+
function sameState(a, b) {
|
|
135
|
+
return Object.is(a.value, b.value) && a.phase === b.phase && a.streamID === b.streamID;
|
|
136
|
+
}
|
|
137
|
+
function durableState(base) {
|
|
138
|
+
return {
|
|
152
139
|
value: base,
|
|
153
140
|
phase: "durable",
|
|
154
141
|
streamID: null
|
|
155
142
|
};
|
|
156
|
-
return lastPrimitiveState;
|
|
157
143
|
}
|
|
158
144
|
//# sourceMappingURL=useStreamingField.native.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["__toCommonJS","mod","__copyProps","__defProp","value","useStreamingField_exports","__export","createUseStreamingField","createUseStreamingFields","module","exports","import_realtime","require","import_react","
|
|
1
|
+
{"version":3,"names":["__toCommonJS","mod","__copyProps","__defProp","value","useStreamingField_exports","__export","createUseStreamingField","createUseStreamingFields","module","exports","import_realtime","require","import_react","getStore","useStreamingField","handle","base","store","id","canonicalTopic","spec","primaryKey","topic","subscribe","useCallback","onChange","last","useRef","getSnapshot","next","durableState","read","cached","current","sameState","useSyncExternalStore","useStreamingFields","requests","topicsKey","useMemo","map","request","key","join","releases","_iteratorNormalCompletion","_didIteratorError","_iteratorError","_iterator","Symbol","iterator","_step","done","release","err","return","cache"],"sources":["../../src/useStreamingField.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createSchema, string, table } from "@rocicorp/zero";
|
|
2
|
+
import { createLocalRealtime, defineStreamingFields } from "orez-lite/realtime";
|
|
3
|
+
import { act } from "react";
|
|
4
|
+
import { createRoot } from "react-dom/client";
|
|
5
|
+
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
|
6
|
+
import { createUseStreamingField, createUseStreamingFields } from "./useStreamingField.mjs";
|
|
7
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
9
|
+
const sootTask = table("sootTask").columns({
|
|
10
|
+
id: string(),
|
|
11
|
+
title: string(),
|
|
12
|
+
description: string()
|
|
13
|
+
}).primaryKey("id");
|
|
14
|
+
const schema = createSchema({
|
|
15
|
+
tables: [sootTask]
|
|
16
|
+
});
|
|
17
|
+
const streaming = defineStreamingFields(schema, {
|
|
18
|
+
sootTask: {
|
|
19
|
+
description: {
|
|
20
|
+
maxBytes: 1e5,
|
|
21
|
+
maxUpdatesPerSecond: 30,
|
|
22
|
+
maxBytesPerSecond: 2e5
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
let container;
|
|
27
|
+
let root;
|
|
28
|
+
let consoleError;
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
container = document.createElement("div");
|
|
31
|
+
document.body.appendChild(container);
|
|
32
|
+
root = createRoot(container);
|
|
33
|
+
consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
34
|
+
});
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
act(() => root.unmount());
|
|
37
|
+
container.remove();
|
|
38
|
+
consoleError.mockRestore();
|
|
39
|
+
});
|
|
40
|
+
const uncachedSnapshotWarnings = () => consoleError.mock.calls.filter(call => String(call[0]).includes("getSnapshot should be cached"));
|
|
41
|
+
test("single-field hook is snapshot-stable while its topic is unsubscribed", () => {
|
|
42
|
+
const realtime = createLocalRealtime({
|
|
43
|
+
manifest: streaming.manifest
|
|
44
|
+
});
|
|
45
|
+
const useStreamingField = createUseStreamingField(() => realtime.store);
|
|
46
|
+
function Card({
|
|
47
|
+
id,
|
|
48
|
+
base
|
|
49
|
+
}) {
|
|
50
|
+
const state = useStreamingField(streaming.sootTask.description({
|
|
51
|
+
id
|
|
52
|
+
}), base);
|
|
53
|
+
return /* @__PURE__ */jsx("div", {
|
|
54
|
+
children: state.value
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
act(() => {
|
|
58
|
+
root.render(/* @__PURE__ */jsxs(Fragment, {
|
|
59
|
+
children: [/* @__PURE__ */jsx(Card, {
|
|
60
|
+
id: "t1",
|
|
61
|
+
base: ""
|
|
62
|
+
}), /* @__PURE__ */jsx(Card, {
|
|
63
|
+
id: "t2",
|
|
64
|
+
base: "a durable description"
|
|
65
|
+
}), /* @__PURE__ */jsx(Card, {
|
|
66
|
+
id: "t3",
|
|
67
|
+
base: ""
|
|
68
|
+
})]
|
|
69
|
+
}));
|
|
70
|
+
});
|
|
71
|
+
expect(uncachedSnapshotWarnings()).toEqual([]);
|
|
72
|
+
});
|
|
73
|
+
test("multi-field hook is snapshot-stable while topics are unsubscribed", () => {
|
|
74
|
+
const realtime = createLocalRealtime({
|
|
75
|
+
manifest: streaming.manifest
|
|
76
|
+
});
|
|
77
|
+
const useStreamingFields = createUseStreamingFields(() => realtime.store);
|
|
78
|
+
const requests = [{
|
|
79
|
+
key: "t1",
|
|
80
|
+
handle: streaming.sootTask.description({
|
|
81
|
+
id: "t1"
|
|
82
|
+
}),
|
|
83
|
+
base: ""
|
|
84
|
+
}, {
|
|
85
|
+
key: "t2",
|
|
86
|
+
handle: streaming.sootTask.description({
|
|
87
|
+
id: "t2"
|
|
88
|
+
}),
|
|
89
|
+
base: "x"
|
|
90
|
+
}];
|
|
91
|
+
function List() {
|
|
92
|
+
const states = useStreamingFields(requests);
|
|
93
|
+
return /* @__PURE__ */jsx("div", {
|
|
94
|
+
children: Object.keys(states).length
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
act(() => {
|
|
98
|
+
root.render(/* @__PURE__ */jsx(List, {}));
|
|
99
|
+
});
|
|
100
|
+
expect(uncachedSnapshotWarnings()).toEqual([]);
|
|
101
|
+
});
|
|
102
|
+
//# sourceMappingURL=useStreamingField.loop.test.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["createSchema","string","table","createLocalRealtime","defineStreamingFields","act","createRoot","afterEach","beforeEach","expect","test","vi","createUseStreamingField","createUseStreamingFields","Fragment","jsx","jsxs","globalThis","IS_REACT_ACT_ENVIRONMENT","sootTask","columns","id","title","description","primaryKey","schema","tables","streaming","maxBytes","maxUpdatesPerSecond","maxBytesPerSecond","container","root","consoleError","document","createElement","body","appendChild","spyOn","console","mockImplementation","unmount","remove","mockRestore","uncachedSnapshotWarnings","mock","calls","filter","call","String","includes","realtime","manifest","useStreamingField","store","Card","base","state","children","value","render","toEqual","useStreamingFields","requests","key","handle","List","states","Object","keys","length"],"sources":["../../src/useStreamingField.loop.test.tsx"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { createSchema, string, table } from "@rocicorp/zero";
|
|
3
|
+
import { createLocalRealtime, defineStreamingFields } from "orez-lite/realtime";
|
|
4
|
+
import { act } from "react";
|
|
5
|
+
import { createRoot } from "react-dom/client";
|
|
6
|
+
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
|
7
|
+
import { createUseStreamingField, createUseStreamingFields } from "./useStreamingField.native.js";
|
|
8
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
9
|
+
var sootTask = table("sootTask").columns({
|
|
10
|
+
id: string(),
|
|
11
|
+
title: string(),
|
|
12
|
+
description: string()
|
|
13
|
+
}).primaryKey("id");
|
|
14
|
+
var schema = createSchema({
|
|
15
|
+
tables: [sootTask]
|
|
16
|
+
});
|
|
17
|
+
var streaming = defineStreamingFields(schema, {
|
|
18
|
+
sootTask: {
|
|
19
|
+
description: {
|
|
20
|
+
maxBytes: 1e5,
|
|
21
|
+
maxUpdatesPerSecond: 30,
|
|
22
|
+
maxBytesPerSecond: 2e5
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
var container;
|
|
27
|
+
var root;
|
|
28
|
+
var consoleError;
|
|
29
|
+
beforeEach(function () {
|
|
30
|
+
container = document.createElement("div");
|
|
31
|
+
document.body.appendChild(container);
|
|
32
|
+
root = createRoot(container);
|
|
33
|
+
consoleError = vi.spyOn(console, "error").mockImplementation(function () {});
|
|
34
|
+
});
|
|
35
|
+
afterEach(function () {
|
|
36
|
+
act(function () {
|
|
37
|
+
return root.unmount();
|
|
38
|
+
});
|
|
39
|
+
container.remove();
|
|
40
|
+
consoleError.mockRestore();
|
|
41
|
+
});
|
|
42
|
+
var uncachedSnapshotWarnings = function uncachedSnapshotWarnings2() {
|
|
43
|
+
return consoleError.mock.calls.filter(function (call) {
|
|
44
|
+
return String(call[0]).includes("getSnapshot should be cached");
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
test("single-field hook is snapshot-stable while its topic is unsubscribed", function () {
|
|
48
|
+
var realtime = createLocalRealtime({
|
|
49
|
+
manifest: streaming.manifest
|
|
50
|
+
});
|
|
51
|
+
var useStreamingField = createUseStreamingField(function () {
|
|
52
|
+
return realtime.store;
|
|
53
|
+
});
|
|
54
|
+
function Card(param) {
|
|
55
|
+
var {
|
|
56
|
+
id,
|
|
57
|
+
base
|
|
58
|
+
} = param;
|
|
59
|
+
var state = useStreamingField(streaming.sootTask.description({
|
|
60
|
+
id
|
|
61
|
+
}), base);
|
|
62
|
+
return /* @__PURE__ */_jsx("div", {
|
|
63
|
+
children: state.value
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
act(function () {
|
|
67
|
+
root.render(/* @__PURE__ */_jsxs(_Fragment, {
|
|
68
|
+
children: [/* @__PURE__ */_jsx(Card, {
|
|
69
|
+
id: "t1",
|
|
70
|
+
base: ""
|
|
71
|
+
}), /* @__PURE__ */_jsx(Card, {
|
|
72
|
+
id: "t2",
|
|
73
|
+
base: "a durable description"
|
|
74
|
+
}), /* @__PURE__ */_jsx(Card, {
|
|
75
|
+
id: "t3",
|
|
76
|
+
base: ""
|
|
77
|
+
})]
|
|
78
|
+
}));
|
|
79
|
+
});
|
|
80
|
+
expect(uncachedSnapshotWarnings()).toEqual([]);
|
|
81
|
+
});
|
|
82
|
+
test("multi-field hook is snapshot-stable while topics are unsubscribed", function () {
|
|
83
|
+
var realtime = createLocalRealtime({
|
|
84
|
+
manifest: streaming.manifest
|
|
85
|
+
});
|
|
86
|
+
var useStreamingFields = createUseStreamingFields(function () {
|
|
87
|
+
return realtime.store;
|
|
88
|
+
});
|
|
89
|
+
var requests = [{
|
|
90
|
+
key: "t1",
|
|
91
|
+
handle: streaming.sootTask.description({
|
|
92
|
+
id: "t1"
|
|
93
|
+
}),
|
|
94
|
+
base: ""
|
|
95
|
+
}, {
|
|
96
|
+
key: "t2",
|
|
97
|
+
handle: streaming.sootTask.description({
|
|
98
|
+
id: "t2"
|
|
99
|
+
}),
|
|
100
|
+
base: "x"
|
|
101
|
+
}];
|
|
102
|
+
function List() {
|
|
103
|
+
var states = useStreamingFields(requests);
|
|
104
|
+
return /* @__PURE__ */_jsx("div", {
|
|
105
|
+
children: Object.keys(states).length
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
act(function () {
|
|
109
|
+
root.render(/* @__PURE__ */_jsx(List, {}));
|
|
110
|
+
});
|
|
111
|
+
expect(uncachedSnapshotWarnings()).toEqual([]);
|
|
112
|
+
});
|
|
113
|
+
//# sourceMappingURL=useStreamingField.loop.test.native.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["jsx","_jsx","jsxs","_jsxs","Fragment","_Fragment","createSchema","string","table","createLocalRealtime","defineStreamingFields","act","createRoot","afterEach","beforeEach","expect","test","vi","createUseStreamingField","createUseStreamingFields","globalThis","IS_REACT_ACT_ENVIRONMENT","sootTask","columns","id","title","description","primaryKey","schema","tables","streaming","maxBytes","maxUpdatesPerSecond","maxBytesPerSecond","container","root","consoleError","document","createElement","body","appendChild","spyOn","console","mockImplementation","unmount","remove","mockRestore","uncachedSnapshotWarnings","uncachedSnapshotWarnings2","mock","calls","filter","call","String","includes","realtime","manifest","useStreamingField","store","Card","param","base","state","children","value","render"],"sources":["../../src/useStreamingField.loop.test.tsx"],"sourcesContent":[null],"mappings":"AAeA,SAASA,GAAA,IAAAC,IAAA,EAAAC,IAAc,IAAAC,KAAQ,EAAAC,QAAa,IAAAC,SAAA;AAC5C,SAASC,YAAA,EAAAC,MAAA,EAAqBC,KAAA,wBAA6B;AAC3D,SAASC,mBAAW,EAAAC,qBAAA;AACpB,SAASC,GAAA,eAA6B;AACtC,SAASC,UAAA,QAAW,kBAAoB;AAExC,SAASC,SAAA,EAAAC,UAAA,EAAAC,MAAyB,EAAAC,IAAA,EAAAC,EAAA;AAmDvB,SAQLC,uBAAA,EAAAC,wBARK;AA7CXC,UAAA,CAAWC,wBAAA,GAA2B;AAEtC,IAAAC,QAAM,GAAAd,KAAW,WAAM,EAAUe,OAC9B;EAEHC,EAAA,EAAMjB,MAAA;EAENkB,KAAM,EAAAlB,MAAA;EACJmB,WAAU,EAAAnB,MAAA;AAAA,GAAAoB,UACR,KAAa;AAAA,IAAAC,MACX,GAAAtB,YAAU;EAAAuB,MACV,GAAqBP,QACrB;AACF,EACF;AACF,IAACQ,SAAA,GAAApB,qBAAA,CAAAkB,MAAA;EAEDN,QAAI;IACAI,WAAA;MACAK,QAAA;MAEJC,mBAAiB;MACfC,iBAAqB;IACrB;EACA;AACA;AAAmE,IAAEC,SAAA;AACvE,IAACC,IAAA;AAED,IAAAC,YAAgB;AACdtB,UAAI,aAAW;EACfoB,SAAA,GAAUG,QAAO,CAAAC,aAAA;EACjBD,QAAA,CAAAE,IAAa,CAAAC,WAAY,CAAAN,SAAA;EAC1BC,IAAA,GAAAvB,UAAA,CAAAsB,SAAA;EAEDE,YAAM,GAAAnB,EAAA,CAAAwB,KAAA,CAAAC,OAA2B,SAC/B,EAAAC,kBAAkB,CAAM,aAAO,CAAC;AAEhC;AAEF9B,SAAK;EACHF,GAAA,aAAM;IACN,OAAMwB,IAAA,CAAAS,OAAA;EAEN;EACEV,SAAM,CAAAW,MAAQ;EACdT,YAAO,CAAAU,WAAA;AAAkB,EAC3B;AAEA,IAAAC,wBAAU,YAAAC,0BAAA;EAIR,OAAKZ,YAAA,CAAAa,IAAA,CAAAC,KAAA,CAAAC,MAAA,WAAAC,IAAA;IAAA,OACHC,MAAA,CAAAD,IAAA,KAAAE,QAAA,+BACE;EAAA;AAAsB;AACqBtC,IAAA,uEACrB;EAAA,IAAAuC,QACxB,GAAA9C,mBAAA;IACF+C,QAAA,EAAA1B,SAAA,CAAA0B;EACF,CAAC;EAED,IAAAC,iBAAO,GAAAvC,uBAAsC;IAC9C,OAAAqC,QAAA,CAAAG,KAAA;EAED;EACE,SAAMC,KAAAC,KAAW;IACjB,IAAM;MAAApC,EAAA;MAAAqC;IAAA,IAAAD,KAAA;IAEN,IAAME,KAAA,GAAAL,iBAAW,CAAA3B,SAAA,CAAAR,QAAA,CAAAI,WAAA;MACbF;IACF,EAAE,EAAAqC,IAAK;IACT,sBAAA5D,IAAA;MAEA8D,QAAS,EAAAD,KAAO,CAAAE;IACd;EACA;EACFrD,GAAA;IAEAwB,IAAI,CAAA8B,MAAM,gBAAA9D,KAAA,CAAAE,SAAA;MACR0D,QAAK,EAAO,CACb,eAAA9D,IAAA,CAAA0D,IAAA;QAEMnC,EAAA;QACRqC,IAAA","ignoreList":[]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { canonicalTopic } from "orez-
|
|
2
|
-
import { useCallback, useMemo, useSyncExternalStore } from "react";
|
|
1
|
+
import { canonicalTopic } from "orez-lite/realtime";
|
|
2
|
+
import { useCallback, useMemo, useRef, useSyncExternalStore } from "react";
|
|
3
3
|
function createUseStreamingField(getStore) {
|
|
4
4
|
return function useStreamingField(handle, base) {
|
|
5
5
|
const store = getStore();
|
|
@@ -8,9 +8,13 @@ function createUseStreamingField(getStore) {
|
|
|
8
8
|
if (!store || !handle) return () => {};
|
|
9
9
|
return store.subscribe(handle, onChange);
|
|
10
10
|
}, [store, id]);
|
|
11
|
+
const last = useRef(void 0);
|
|
11
12
|
const getSnapshot = useCallback(() => {
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
const next = !store || !handle ? durableState(base) : store.read(handle, base);
|
|
14
|
+
const cached = last.current;
|
|
15
|
+
if (cached && sameState(cached, next)) return cached;
|
|
16
|
+
last.current = next;
|
|
17
|
+
return next;
|
|
14
18
|
}, [store, id, base]);
|
|
15
19
|
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
16
20
|
};
|
|
@@ -30,15 +34,18 @@ function createUseStreamingFields(getStore) {
|
|
|
30
34
|
value: {}
|
|
31
35
|
}), [store, topicsKey]);
|
|
32
36
|
const getSnapshot = useCallback(() => {
|
|
37
|
+
const previous = cache.value;
|
|
33
38
|
const next = {};
|
|
34
39
|
let changed = false;
|
|
35
40
|
for (const request of requests) {
|
|
36
|
-
const
|
|
41
|
+
const raw = store ? store.read(request.handle, request.base) : durableState(request.base);
|
|
42
|
+
const cached = previous[request.key];
|
|
43
|
+
const state = cached && sameState(cached, raw) ? cached : raw;
|
|
37
44
|
next[request.key] = state;
|
|
38
|
-
if (
|
|
45
|
+
if (state !== cached) changed = true;
|
|
39
46
|
}
|
|
40
|
-
if (!changed && Object.keys(
|
|
41
|
-
return
|
|
47
|
+
if (!changed && Object.keys(previous).length === Object.keys(next).length) {
|
|
48
|
+
return previous;
|
|
42
49
|
}
|
|
43
50
|
cache.value = next;
|
|
44
51
|
return next;
|
|
@@ -46,31 +53,15 @@ function createUseStreamingFields(getStore) {
|
|
|
46
53
|
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
47
54
|
};
|
|
48
55
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
const cached = DURABLE_CACHE.get(base);
|
|
55
|
-
if (cached) return cached;
|
|
56
|
-
const state = {
|
|
57
|
-
value: base,
|
|
58
|
-
phase: "durable",
|
|
59
|
-
streamID: null
|
|
60
|
-
};
|
|
61
|
-
DURABLE_CACHE.set(base, state);
|
|
62
|
-
return state;
|
|
63
|
-
}
|
|
64
|
-
if (lastPrimitiveState && Object.is(lastPrimitiveBase, base)) {
|
|
65
|
-
return lastPrimitiveState;
|
|
66
|
-
}
|
|
67
|
-
lastPrimitiveBase = base;
|
|
68
|
-
lastPrimitiveState = {
|
|
56
|
+
function sameState(a, b) {
|
|
57
|
+
return Object.is(a.value, b.value) && a.phase === b.phase && a.streamID === b.streamID;
|
|
58
|
+
}
|
|
59
|
+
function durableState(base) {
|
|
60
|
+
return {
|
|
69
61
|
value: base,
|
|
70
62
|
phase: "durable",
|
|
71
63
|
streamID: null
|
|
72
64
|
};
|
|
73
|
-
return lastPrimitiveState;
|
|
74
65
|
}
|
|
75
66
|
export { createUseStreamingField, createUseStreamingFields };
|
|
76
67
|
//# sourceMappingURL=useStreamingField.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["canonicalTopic","useCallback","useMemo","useSyncExternalStore","createUseStreamingField","getStore","useStreamingField","handle","base","store","id","spec","primaryKey","topic","subscribe","onChange","getSnapshot","
|
|
1
|
+
{"version":3,"names":["canonicalTopic","useCallback","useMemo","useRef","useSyncExternalStore","createUseStreamingField","getStore","useStreamingField","handle","base","store","id","spec","primaryKey","topic","subscribe","onChange","last","getSnapshot","next","durableState","read","cached","current","sameState","createUseStreamingFields","useStreamingFields","requests","topicsKey","map","request","key","join","releases","release","cache","value","previous","changed","raw","state","Object","keys","length","a","b","is","phase","streamID"],"sources":["../../src/useStreamingField.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
|
|
@@ -1,10 +1,5 @@
|
|
|
1
|
-
import { canonicalTopic } from "orez-
|
|
2
|
-
import { useCallback, useMemo, useSyncExternalStore } from "react";
|
|
3
|
-
function _type_of(obj) {
|
|
4
|
-
"@swc/helpers - typeof";
|
|
5
|
-
|
|
6
|
-
return obj && typeof Symbol !== "undefined" && obj.constructor === Symbol ? "symbol" : typeof obj;
|
|
7
|
-
}
|
|
1
|
+
import { canonicalTopic } from "orez-lite/realtime";
|
|
2
|
+
import { useCallback, useMemo, useRef, useSyncExternalStore } from "react";
|
|
8
3
|
function createUseStreamingField(getStore) {
|
|
9
4
|
return function useStreamingField(handle, base) {
|
|
10
5
|
var store = getStore();
|
|
@@ -13,9 +8,13 @@ function createUseStreamingField(getStore) {
|
|
|
13
8
|
if (!store || !handle) return function () {};
|
|
14
9
|
return store.subscribe(handle, onChange);
|
|
15
10
|
}, [store, id]);
|
|
11
|
+
var last = useRef(void 0);
|
|
16
12
|
var getSnapshot = useCallback(function () {
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
var next = !store || !handle ? durableState(base) : store.read(handle, base);
|
|
14
|
+
var cached = last.current;
|
|
15
|
+
if (cached && sameState(cached, next)) return cached;
|
|
16
|
+
last.current = next;
|
|
17
|
+
return next;
|
|
19
18
|
}, [store, id, base]);
|
|
20
19
|
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
21
20
|
};
|
|
@@ -64,6 +63,7 @@ function createUseStreamingFields(getStore) {
|
|
|
64
63
|
};
|
|
65
64
|
}, [store, topicsKey]);
|
|
66
65
|
var getSnapshot = useCallback(function () {
|
|
66
|
+
var previous = cache.value;
|
|
67
67
|
var next = {};
|
|
68
68
|
var changed = false;
|
|
69
69
|
var _iteratorNormalCompletion = true,
|
|
@@ -72,9 +72,11 @@ function createUseStreamingFields(getStore) {
|
|
|
72
72
|
try {
|
|
73
73
|
for (var _iterator = requests[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
|
|
74
74
|
var request = _step.value;
|
|
75
|
-
var
|
|
75
|
+
var raw = store ? store.read(request.handle, request.base) : durableState(request.base);
|
|
76
|
+
var cached = previous[request.key];
|
|
77
|
+
var state = cached && sameState(cached, raw) ? cached : raw;
|
|
76
78
|
next[request.key] = state;
|
|
77
|
-
if (
|
|
79
|
+
if (state !== cached) changed = true;
|
|
78
80
|
}
|
|
79
81
|
} catch (err) {
|
|
80
82
|
_didIteratorError = true;
|
|
@@ -90,8 +92,8 @@ function createUseStreamingFields(getStore) {
|
|
|
90
92
|
}
|
|
91
93
|
}
|
|
92
94
|
}
|
|
93
|
-
if (!changed && Object.keys(
|
|
94
|
-
return
|
|
95
|
+
if (!changed && Object.keys(previous).length === Object.keys(next).length) {
|
|
96
|
+
return previous;
|
|
95
97
|
}
|
|
96
98
|
cache.value = next;
|
|
97
99
|
return next;
|
|
@@ -99,31 +101,15 @@ function createUseStreamingFields(getStore) {
|
|
|
99
101
|
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
100
102
|
};
|
|
101
103
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
function
|
|
106
|
-
|
|
107
|
-
var cached = DURABLE_CACHE.get(base);
|
|
108
|
-
if (cached) return cached;
|
|
109
|
-
var state = {
|
|
110
|
-
value: base,
|
|
111
|
-
phase: "durable",
|
|
112
|
-
streamID: null
|
|
113
|
-
};
|
|
114
|
-
DURABLE_CACHE.set(base, state);
|
|
115
|
-
return state;
|
|
116
|
-
}
|
|
117
|
-
if (lastPrimitiveState && Object.is(lastPrimitiveBase, base)) {
|
|
118
|
-
return lastPrimitiveState;
|
|
119
|
-
}
|
|
120
|
-
lastPrimitiveBase = base;
|
|
121
|
-
lastPrimitiveState = {
|
|
104
|
+
function sameState(a, b) {
|
|
105
|
+
return Object.is(a.value, b.value) && a.phase === b.phase && a.streamID === b.streamID;
|
|
106
|
+
}
|
|
107
|
+
function durableState(base) {
|
|
108
|
+
return {
|
|
122
109
|
value: base,
|
|
123
110
|
phase: "durable",
|
|
124
111
|
streamID: null
|
|
125
112
|
};
|
|
126
|
-
return lastPrimitiveState;
|
|
127
113
|
}
|
|
128
114
|
export { createUseStreamingField, createUseStreamingFields };
|
|
129
115
|
//# sourceMappingURL=useStreamingField.native.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["canonicalTopic","useCallback","useMemo","
|
|
1
|
+
{"version":3,"names":["canonicalTopic","useCallback","useMemo","useRef","useSyncExternalStore","createUseStreamingField","getStore","useStreamingField","handle","base","store","id","spec","primaryKey","topic","subscribe","onChange","last","getSnapshot","next","durableState","read","cached","current","sameState","createUseStreamingFields","useStreamingFields","requests","topicsKey","map","request","key","join","releases","_iteratorNormalCompletion","_didIteratorError","_iteratorError","_iterator","Symbol","iterator","_step","done","release","value","err","return","cache"],"sources":["../../src/useStreamingField.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "on-zero",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.3",
|
|
4
4
|
"description": "A typed layer over @rocicorp/zero with queries, mutations, and permissions",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"files": [
|
|
@@ -105,8 +105,7 @@
|
|
|
105
105
|
"chokidar": "^4.0.3",
|
|
106
106
|
"citty": "^0.1.6",
|
|
107
107
|
"dequal": "^2.0.3",
|
|
108
|
-
"orez-lite": "0.11.
|
|
109
|
-
"orez-sync-executor": "0.11.2",
|
|
108
|
+
"orez-lite": "0.11.3",
|
|
110
109
|
"valibot": "^1.1.0"
|
|
111
110
|
},
|
|
112
111
|
"peerDependencies": {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
//
|
|
3
|
+
// useSyncExternalStore's getSnapshot contract: the same reference must come
|
|
4
|
+
// back until something observable changes. React DEV enforces it by calling
|
|
5
|
+
// getSnapshot twice per render and warning "The result of getSnapshot should
|
|
6
|
+
// be cached to avoid an infinite loop"; in production the same instability is
|
|
7
|
+
// a forceStoreRerender loop that throws "Maximum update depth exceeded".
|
|
8
|
+
//
|
|
9
|
+
// The store only stabilizes SUBSCRIBED topics. An unsubscribed read — the
|
|
10
|
+
// render before the subscription effect lands, or every render after an error
|
|
11
|
+
// boundary keeps that effect from ever landing — built a fresh state object
|
|
12
|
+
// per call, and a factory workspace rendering a task card with an empty
|
|
13
|
+
// description looped React until the whole pane tree unmounted. The hooks now
|
|
14
|
+
// carry their own per-hook stabilization, which this file locks in.
|
|
15
|
+
|
|
16
|
+
import { createSchema, string, table } from '@rocicorp/zero'
|
|
17
|
+
import { createLocalRealtime, defineStreamingFields } from 'orez-lite/realtime'
|
|
18
|
+
import { act } from 'react'
|
|
19
|
+
import { createRoot, type Root } from 'react-dom/client'
|
|
20
|
+
import { afterEach, beforeEach, expect, test, vi } from 'vitest'
|
|
21
|
+
|
|
22
|
+
import { createUseStreamingField, createUseStreamingFields } from './useStreamingField'
|
|
23
|
+
|
|
24
|
+
declare global {
|
|
25
|
+
// eslint-disable-next-line no-var
|
|
26
|
+
var IS_REACT_ACT_ENVIRONMENT: boolean | undefined
|
|
27
|
+
}
|
|
28
|
+
globalThis.IS_REACT_ACT_ENVIRONMENT = true
|
|
29
|
+
|
|
30
|
+
const sootTask = table('sootTask')
|
|
31
|
+
.columns({ id: string(), title: string(), description: string() })
|
|
32
|
+
.primaryKey('id')
|
|
33
|
+
const schema = createSchema({ tables: [sootTask] })
|
|
34
|
+
|
|
35
|
+
const streaming = defineStreamingFields(schema, {
|
|
36
|
+
sootTask: {
|
|
37
|
+
description: {
|
|
38
|
+
maxBytes: 100_000,
|
|
39
|
+
maxUpdatesPerSecond: 30,
|
|
40
|
+
maxBytesPerSecond: 200_000,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
let container: HTMLDivElement
|
|
46
|
+
let root: Root
|
|
47
|
+
let consoleError: ReturnType<typeof vi.spyOn>
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
container = document.createElement('div')
|
|
51
|
+
document.body.appendChild(container)
|
|
52
|
+
root = createRoot(container)
|
|
53
|
+
consoleError = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
afterEach(() => {
|
|
57
|
+
act(() => root.unmount())
|
|
58
|
+
container.remove()
|
|
59
|
+
consoleError.mockRestore()
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const uncachedSnapshotWarnings = () =>
|
|
63
|
+
consoleError.mock.calls.filter((call) =>
|
|
64
|
+
String(call[0]).includes('getSnapshot should be cached')
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
test('single-field hook is snapshot-stable while its topic is unsubscribed', () => {
|
|
68
|
+
const realtime = createLocalRealtime({ manifest: streaming.manifest })
|
|
69
|
+
const useStreamingField = createUseStreamingField(() => realtime.store)
|
|
70
|
+
|
|
71
|
+
function Card({ id, base }: { id: string; base: string }) {
|
|
72
|
+
const state = useStreamingField(streaming.sootTask.description({ id }), base)
|
|
73
|
+
return <div>{state.value}</div>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
act(() => {
|
|
77
|
+
// several consumers with DIFFERENT bases interleave in one render pass, so
|
|
78
|
+
// a shared module-level cache cannot satisfy the contract — only per-hook
|
|
79
|
+
// stabilization can
|
|
80
|
+
root.render(
|
|
81
|
+
<>
|
|
82
|
+
<Card id="t1" base="" />
|
|
83
|
+
<Card id="t2" base="a durable description" />
|
|
84
|
+
<Card id="t3" base="" />
|
|
85
|
+
</>
|
|
86
|
+
)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
expect(uncachedSnapshotWarnings()).toEqual([])
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('multi-field hook is snapshot-stable while topics are unsubscribed', () => {
|
|
93
|
+
const realtime = createLocalRealtime({ manifest: streaming.manifest })
|
|
94
|
+
const useStreamingFields = createUseStreamingFields(() => realtime.store)
|
|
95
|
+
|
|
96
|
+
const requests = [
|
|
97
|
+
{ key: 't1', handle: streaming.sootTask.description({ id: 't1' }), base: '' },
|
|
98
|
+
{ key: 't2', handle: streaming.sootTask.description({ id: 't2' }), base: 'x' },
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
function List() {
|
|
102
|
+
const states = useStreamingFields(requests)
|
|
103
|
+
return <div>{Object.keys(states).length}</div>
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
act(() => {
|
|
107
|
+
root.render(<List />)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
expect(uncachedSnapshotWarnings()).toEqual([])
|
|
111
|
+
})
|
package/src/useStreamingField.ts
CHANGED
|
@@ -17,14 +17,14 @@
|
|
|
17
17
|
// is the property an app hand-rolls otherwise, usually as a global emitter plus
|
|
18
18
|
// a deep-equal projection to undo the over-broadcasting.
|
|
19
19
|
|
|
20
|
-
import { canonicalTopic } from 'orez-
|
|
21
|
-
import { useCallback, useMemo, useSyncExternalStore } from 'react'
|
|
20
|
+
import { canonicalTopic } from 'orez-lite/realtime'
|
|
21
|
+
import { useCallback, useMemo, useRef, useSyncExternalStore } from 'react'
|
|
22
22
|
|
|
23
23
|
import type {
|
|
24
24
|
RealtimeStore,
|
|
25
25
|
StreamingFieldHandle,
|
|
26
26
|
StreamingFieldState,
|
|
27
|
-
} from 'orez-
|
|
27
|
+
} from 'orez-lite/realtime'
|
|
28
28
|
|
|
29
29
|
// `streaming.message.content({ id })` returns a StreamingFieldHandle: the row's
|
|
30
30
|
// topic plus the manifest spec for that column, so a hook needs no lookup and
|
|
@@ -75,13 +75,23 @@ export function createUseStreamingField(
|
|
|
75
75
|
[store, id]
|
|
76
76
|
)
|
|
77
77
|
|
|
78
|
-
//
|
|
79
|
-
// re-invokes
|
|
80
|
-
//
|
|
81
|
-
//
|
|
78
|
+
// getSnapshot must hand back the SAME reference until something observable
|
|
79
|
+
// changes: useSyncExternalStore re-invokes it on every render and rerenders
|
|
80
|
+
// forever (throwing "Maximum update depth exceeded") if each call returns a
|
|
81
|
+
// fresh object. The store only stabilizes SUBSCRIBED topics — an
|
|
82
|
+
// unsubscribed read (before the subscription effect lands, after an error
|
|
83
|
+
// boundary unmounts the tree so it never lands, or with a null handle) has
|
|
84
|
+
// no entry to cache on. Each hook therefore carries its own last-state
|
|
85
|
+
// slot, which also survives interleaved renders of many hooks in a way a
|
|
86
|
+
// module-level cache cannot. `base` participates because a new durable
|
|
87
|
+
// value from Zero is what ends the committing phase.
|
|
88
|
+
const last = useRef<StreamingFieldState<Value> | undefined>(undefined)
|
|
82
89
|
const getSnapshot = useCallback(() => {
|
|
83
|
-
|
|
84
|
-
|
|
90
|
+
const next = !store || !handle ? durableState(base) : store.read(handle, base)
|
|
91
|
+
const cached = last.current
|
|
92
|
+
if (cached && sameState(cached, next)) return cached
|
|
93
|
+
last.current = next
|
|
94
|
+
return next
|
|
85
95
|
}, [store, id, base])
|
|
86
96
|
|
|
87
97
|
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot)
|
|
@@ -138,17 +148,24 @@ export function createUseStreamingFields(
|
|
|
138
148
|
)
|
|
139
149
|
|
|
140
150
|
const getSnapshot = useCallback(() => {
|
|
151
|
+
const previous = cache.value
|
|
141
152
|
const next: Record<string, StreamingFieldState<Value>> = {}
|
|
142
153
|
let changed = false
|
|
143
154
|
for (const request of requests) {
|
|
144
|
-
const
|
|
155
|
+
const raw = store
|
|
145
156
|
? store.read(request.handle, request.base)
|
|
146
|
-
:
|
|
157
|
+
: durableState(request.base)
|
|
158
|
+
// stabilize per key here, not just per store entry: an unsubscribed
|
|
159
|
+
// topic's read is a fresh object each call, and one unstable member
|
|
160
|
+
// would otherwise force a fresh map from every getSnapshot — the same
|
|
161
|
+
// rerender loop the single-field hook guards against.
|
|
162
|
+
const cached = previous[request.key]
|
|
163
|
+
const state = cached && sameState(cached, raw) ? cached : raw
|
|
147
164
|
next[request.key] = state
|
|
148
|
-
if (
|
|
165
|
+
if (state !== cached) changed = true
|
|
149
166
|
}
|
|
150
|
-
if (!changed && Object.keys(
|
|
151
|
-
return
|
|
167
|
+
if (!changed && Object.keys(previous).length === Object.keys(next).length) {
|
|
168
|
+
return previous
|
|
152
169
|
}
|
|
153
170
|
cache.value = next
|
|
154
171
|
return next
|
|
@@ -158,23 +175,16 @@ export function createUseStreamingFields(
|
|
|
158
175
|
}
|
|
159
176
|
}
|
|
160
177
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
174
|
-
if (lastPrimitiveState && Object.is(lastPrimitiveBase, base)) {
|
|
175
|
-
return lastPrimitiveState as StreamingFieldState<Value>
|
|
176
|
-
}
|
|
177
|
-
lastPrimitiveBase = base
|
|
178
|
-
lastPrimitiveState = { value: base, phase: 'durable', streamID: null }
|
|
179
|
-
return lastPrimitiveState as StreamingFieldState<Value>
|
|
178
|
+
function sameState(
|
|
179
|
+
a: StreamingFieldState<unknown>,
|
|
180
|
+
b: StreamingFieldState<unknown>
|
|
181
|
+
): boolean {
|
|
182
|
+
// exact: `value` is either the caller's own base reference or the store
|
|
183
|
+
// generation's accumulated value, both of which only change identity when
|
|
184
|
+
// they actually change
|
|
185
|
+
return Object.is(a.value, b.value) && a.phase === b.phase && a.streamID === b.streamID
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function durableState<Value>(base: Value): StreamingFieldState<Value> {
|
|
189
|
+
return { value: base, phase: 'durable', streamID: null }
|
|
180
190
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RealtimeStore, StreamingFieldHandle, StreamingFieldState } from 'orez-
|
|
1
|
+
import type { RealtimeStore, StreamingFieldHandle, StreamingFieldState } from 'orez-lite/realtime';
|
|
2
2
|
export type { StreamingFieldHandle };
|
|
3
3
|
export type UseStreamingField = <Value>(handle: StreamingFieldHandle | null | undefined, base: Value) => StreamingFieldState<Value>;
|
|
4
4
|
export type StreamingFieldRequest<Value = unknown> = {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useStreamingField.d.ts","sourceRoot":"","sources":["../src/useStreamingField.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EACV,aAAa,EACb,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,
|
|
1
|
+
{"version":3,"file":"useStreamingField.d.ts","sourceRoot":"","sources":["../src/useStreamingField.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EACV,aAAa,EACb,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,oBAAoB,CAAA;AAK3B,YAAY,EAAE,oBAAoB,EAAE,CAAA;AAKpC,MAAM,MAAM,iBAAiB,GAAG,CAAC,KAAK,EACpC,MAAM,EAAE,oBAAoB,GAAG,IAAI,GAAG,SAAS,EAC/C,IAAI,EAAE,KAAK,KACR,mBAAmB,CAAC,KAAK,CAAC,CAAA;AAK/B,MAAM,MAAM,qBAAqB,CAAC,KAAK,GAAG,OAAO,IAAI;IACnD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,MAAM,EAAE,oBAAoB,CAAA;IACrC,QAAQ,CAAC,IAAI,EAAE,KAAK,CAAA;CACrB,CAAA;AAED,MAAM,MAAM,kBAAkB,GAAG,CAAC,KAAK,EACrC,QAAQ,EAAE,SAAS,qBAAqB,CAAC,KAAK,CAAC,EAAE,KAC9C,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AAKzD,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,aAAa,GAAG,SAAS,GACxC,iBAAiB,CAuCnB;AASD,wBAAgB,wBAAwB,CACtC,QAAQ,EAAE,MAAM,aAAa,GAAG,SAAS,GACxC,kBAAkB,CAkEpB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useStreamingField.loop.test.d.ts","sourceRoot":"","sources":["../src/useStreamingField.loop.test.tsx"],"names":[],"mappings":"AAuBA,OAAO,CAAC,MAAM,CAAC;IAEb,IAAI,wBAAwB,EAAE,OAAO,GAAG,SAAS,CAAA;CAClD"}
|