teamplay 0.5.0-alpha.13 → 0.5.0-alpha.15
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/orm/Compat/hooksCompat.js +0 -2
- package/dist/react/convertToObserver.js +1 -4
- package/dist/react/renderAttemptDestroyer.d.ts +0 -8
- package/dist/react/renderAttemptDestroyer.js +2 -28
- package/dist/react/trapRender.js +2 -2
- package/dist/react/useSub.d.ts +17 -5
- package/dist/react/useSub.js +24 -28
- package/dist/react/useSuspendMemo.js +1 -5
- package/package.json +2 -2
- package/dist/react/compatComponentRegistry.d.ts +0 -4
- package/dist/react/compatComponentRegistry.js +0 -19
|
@@ -247,7 +247,6 @@ function isExtraQuery(query) {
|
|
|
247
247
|
const BATCH_SUB_OPTIONS = Object.freeze({
|
|
248
248
|
async: false,
|
|
249
249
|
batch: true,
|
|
250
|
-
compatAttemptCleanup: true,
|
|
251
250
|
// Batch hooks are a hard suspense barrier. Deferred params can skip the barrier
|
|
252
251
|
// on route transitions and cause immediate reads from stale/empty local nodes.
|
|
253
252
|
defer: false
|
|
@@ -259,7 +258,6 @@ function normalizeSyncSubOptions(options) {
|
|
|
259
258
|
return {
|
|
260
259
|
...(options || {}),
|
|
261
260
|
async: false,
|
|
262
|
-
compatAttemptCleanup: true,
|
|
263
261
|
// Compat sync hooks are strict by design: no deferred snapshots between route/tab switches.
|
|
264
262
|
defer: false
|
|
265
263
|
};
|
|
@@ -7,7 +7,6 @@ import executionContextTracker from "./executionContextTracker.js";
|
|
|
7
7
|
import { pipeComponentMeta, useUnmount, useId, useTriggerUpdate } from "./helpers.js";
|
|
8
8
|
import trapRender from './trapRender.js';
|
|
9
9
|
import { scheduleReaction } from '../orm/batchScheduler.js';
|
|
10
|
-
import { isCompatComponent, unmarkCompatComponent } from "./compatComponentRegistry.js";
|
|
11
10
|
const DEFAULT_THROTTLE_TIMEOUT = 100;
|
|
12
11
|
export default function convertToObserver(BaseComponent, { forwardRef, cache: enableCache = true, throttle, ...options } = {}) {
|
|
13
12
|
throttle = normalizeThrottle(throttle);
|
|
@@ -33,7 +32,7 @@ export default function convertToObserver(BaseComponent, { forwardRef, cache: en
|
|
|
33
32
|
hasDeferredUpdateAfterExecutionContext = false;
|
|
34
33
|
triggerUpdate();
|
|
35
34
|
}
|
|
36
|
-
else
|
|
35
|
+
else {
|
|
37
36
|
if (hasDeferredUpdateAfterExecutionContext)
|
|
38
37
|
return;
|
|
39
38
|
hasDeferredUpdateAfterExecutionContext = true;
|
|
@@ -53,7 +52,6 @@ export default function convertToObserver(BaseComponent, { forwardRef, cache: en
|
|
|
53
52
|
if (!reactionRef.current)
|
|
54
53
|
throw Error(`NO REACTION REF - ${where}`);
|
|
55
54
|
destroyRef.current = undefined;
|
|
56
|
-
unmarkCompatComponent(componentId);
|
|
57
55
|
unobserve(reactionRef.current);
|
|
58
56
|
reactionRef.current = undefined;
|
|
59
57
|
destroyCache(where);
|
|
@@ -71,7 +69,6 @@ export default function convertToObserver(BaseComponent, { forwardRef, cache: en
|
|
|
71
69
|
}
|
|
72
70
|
// clean up observer on unmount
|
|
73
71
|
useUnmount(() => {
|
|
74
|
-
unmarkCompatComponent(componentId);
|
|
75
72
|
destroyRef.current?.('useUnmount()');
|
|
76
73
|
});
|
|
77
74
|
return reactionRef.current(...args);
|
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
type DestroyAttempt = () => unknown | Promise<unknown>;
|
|
2
1
|
declare class RenderAttemptDestroyer {
|
|
3
|
-
fns: DestroyAttempt[];
|
|
4
|
-
compatAttemptCleanupArmed: boolean;
|
|
5
2
|
suspenseGateArmed: boolean;
|
|
6
3
|
constructor();
|
|
7
|
-
add(fn: DestroyAttempt | undefined, { compat }?: {
|
|
8
|
-
compat?: boolean;
|
|
9
|
-
}): void;
|
|
10
|
-
armCompatAttemptCleanup(): void;
|
|
11
4
|
armSuspenseGate(): void;
|
|
12
5
|
consumeThenableHandling(): {
|
|
13
6
|
shouldKeepShellAlive: boolean;
|
|
14
|
-
destroyAttempt?: () => Promise<void>;
|
|
15
7
|
};
|
|
16
8
|
reset(): void;
|
|
17
9
|
}
|
|
@@ -1,45 +1,19 @@
|
|
|
1
1
|
class RenderAttemptDestroyer {
|
|
2
|
-
fns;
|
|
3
|
-
compatAttemptCleanupArmed;
|
|
4
2
|
suspenseGateArmed;
|
|
5
3
|
constructor() {
|
|
6
|
-
this.fns = [];
|
|
7
|
-
this.compatAttemptCleanupArmed = false;
|
|
8
4
|
this.suspenseGateArmed = false;
|
|
9
5
|
}
|
|
10
|
-
add(fn, { compat = false } = {}) {
|
|
11
|
-
if (typeof fn !== 'function')
|
|
12
|
-
return;
|
|
13
|
-
this.fns.push(fn);
|
|
14
|
-
if (compat)
|
|
15
|
-
this.compatAttemptCleanupArmed = true;
|
|
16
|
-
}
|
|
17
|
-
armCompatAttemptCleanup() {
|
|
18
|
-
this.compatAttemptCleanupArmed = true;
|
|
19
|
-
}
|
|
20
6
|
armSuspenseGate() {
|
|
21
7
|
this.suspenseGateArmed = true;
|
|
22
8
|
}
|
|
23
9
|
consumeThenableHandling() {
|
|
24
|
-
const
|
|
25
|
-
const shouldKeepShellAlive = this.suspenseGateArmed || shouldRunAttemptCleanup;
|
|
26
|
-
let destroyAttempt;
|
|
27
|
-
if (shouldRunAttemptCleanup) {
|
|
28
|
-
const fns = [...this.fns];
|
|
29
|
-
destroyAttempt = async () => {
|
|
30
|
-
await Promise.allSettled(fns.map(fn => fn()));
|
|
31
|
-
fns.length = 0;
|
|
32
|
-
};
|
|
33
|
-
}
|
|
10
|
+
const shouldKeepShellAlive = this.suspenseGateArmed;
|
|
34
11
|
this.reset();
|
|
35
12
|
return {
|
|
36
|
-
shouldKeepShellAlive
|
|
37
|
-
destroyAttempt
|
|
13
|
+
shouldKeepShellAlive
|
|
38
14
|
};
|
|
39
15
|
}
|
|
40
16
|
reset() {
|
|
41
|
-
this.fns.length = 0;
|
|
42
|
-
this.compatAttemptCleanupArmed = false;
|
|
43
17
|
this.suspenseGateArmed = false;
|
|
44
18
|
}
|
|
45
19
|
}
|
package/dist/react/trapRender.js
CHANGED
|
@@ -24,9 +24,9 @@ export default function trapRender({ render, cache, destroy, componentId }) {
|
|
|
24
24
|
destroyed = true;
|
|
25
25
|
throw err;
|
|
26
26
|
}
|
|
27
|
-
const { shouldKeepShellAlive
|
|
27
|
+
const { shouldKeepShellAlive } = renderAttemptDestroyer.consumeThenableHandling();
|
|
28
28
|
if (shouldKeepShellAlive) {
|
|
29
|
-
throw Promise.resolve(err)
|
|
29
|
+
throw Promise.resolve(err);
|
|
30
30
|
}
|
|
31
31
|
// TODO: this might only be needed only if promise is thrown
|
|
32
32
|
// (check if useUnmount in convertToObserver is called if a regular error is thrown)
|
package/dist/react/useSub.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AggregationFunction, AggregationParams, ClientAggregationFunction } from '@teamplay/utils/aggregation';
|
|
2
|
-
import type
|
|
2
|
+
import { type CollectionSignal, type ComputedQueryParamsInput, type DocumentSignal, type QueryParams, type RegisteredAggregationInput, type SignalModelConstructor, type SubResult, type TypedAggregationInput, type TypedAggregationSignal, type WildcardSignalPath } from '../orm/Signal.js';
|
|
3
3
|
export interface UseSubOptions {
|
|
4
4
|
/** Return `undefined` while loading instead of throwing a Suspense promise. */
|
|
5
5
|
async?: boolean;
|
|
@@ -7,9 +7,14 @@ export interface UseSubOptions {
|
|
|
7
7
|
defer?: boolean | number;
|
|
8
8
|
/** Batch Suspense promises across multiple subscriptions in one render attempt. */
|
|
9
9
|
batch?: boolean;
|
|
10
|
-
/** Enable compatibility cleanup for legacy observer render attempts. */
|
|
11
|
-
compatAttemptCleanup?: boolean;
|
|
12
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Subscribe to a document signal in React async mode.
|
|
13
|
+
* @param signal Document signal to subscribe to.
|
|
14
|
+
* @param params Must be omitted for document subscriptions.
|
|
15
|
+
* @param options Subscription behavior options.
|
|
16
|
+
*/
|
|
17
|
+
export declare function useAsyncSub<TSignal extends DocumentSignal<any, any, any>>(signal: TSignal, options?: UseSubOptions): SubResult<TSignal>;
|
|
13
18
|
/**
|
|
14
19
|
* Subscribe to a document signal in React async mode.
|
|
15
20
|
* @param signal Document signal to subscribe to.
|
|
@@ -61,6 +66,13 @@ export declare function useAsyncSub<TDocument, TDocumentModel extends SignalMode
|
|
|
61
66
|
* @param options Subscription behavior options.
|
|
62
67
|
*/
|
|
63
68
|
export declare function useAsyncSub<TOutput = unknown, TCollection extends string = string>(signal: AggregationFunction<TOutput, TCollection>, params?: AggregationParams, options?: UseSubOptions): SubResult<AggregationFunction<TOutput, TCollection>, AggregationParams | undefined>;
|
|
69
|
+
/**
|
|
70
|
+
* Subscribe to a document signal in React.
|
|
71
|
+
* @param signal Document signal to subscribe to.
|
|
72
|
+
* @param params Must be omitted for document subscriptions.
|
|
73
|
+
* @param options Subscription behavior options.
|
|
74
|
+
*/
|
|
75
|
+
export default function useSub<TSignal extends DocumentSignal<any, any, any>>(signal: TSignal, options?: UseSubOptions): SubResult<TSignal>;
|
|
64
76
|
/**
|
|
65
77
|
* Subscribe to a document signal in React.
|
|
66
78
|
* @param signal Document signal to subscribe to.
|
|
@@ -112,8 +124,8 @@ export default function useSub<TDocument, TDocumentModel extends SignalModelCons
|
|
|
112
124
|
* @param options Subscription behavior options.
|
|
113
125
|
*/
|
|
114
126
|
export default function useSub<TOutput = unknown, TCollection extends string = string>(signal: AggregationFunction<TOutput, TCollection>, params?: AggregationParams, options?: UseSubOptions): SubResult<AggregationFunction<TOutput, TCollection>, AggregationParams | undefined>;
|
|
115
|
-
export declare function useSubDeferred(signal: unknown, params?: unknown, { async, defer, batch
|
|
116
|
-
export declare function useSubClassic(signal: unknown, params?: unknown, { async, batch
|
|
127
|
+
export declare function useSubDeferred(signal: unknown, params?: unknown, { async, defer, batch }?: UseSubOptions): unknown;
|
|
128
|
+
export declare function useSubClassic(signal: unknown, params?: unknown, { async, batch }?: UseSubOptions): unknown;
|
|
117
129
|
export declare function setTestThrottling(ms: number): void;
|
|
118
130
|
export declare function resetTestThrottling(): void;
|
|
119
131
|
export declare function setUseDeferredValue(value: boolean): void;
|
package/dist/react/useSub.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { useRef, useDeferredValue } from 'react';
|
|
2
2
|
import sub from "../orm/sub.js";
|
|
3
|
-
import { useScheduleUpdate, useCache, useDefer
|
|
3
|
+
import { useScheduleUpdate, useCache, useDefer } from "./helpers.js";
|
|
4
4
|
import executionContextTracker from "./executionContextTracker.js";
|
|
5
5
|
import * as promiseBatcher from "./promiseBatcher.js";
|
|
6
|
-
import
|
|
7
|
-
import { markCompatComponent } from "./compatComponentRegistry.js";
|
|
6
|
+
import { isPublicDocumentSignal } from "../orm/Signal.js";
|
|
8
7
|
const runtimeSub = sub;
|
|
8
|
+
const USE_SUB_OPTION_KEYS = new Set(['async', 'defer', 'batch']);
|
|
9
9
|
let TEST_THROTTLING = false;
|
|
10
10
|
// experimental feature to leverage useDeferredValue() to handle re-subscriptions.
|
|
11
11
|
// Currently it does lead to issues with extra rerenders and requires further investigation
|
|
@@ -13,10 +13,27 @@ let USE_DEFERRED_VALUE = true;
|
|
|
13
13
|
// by default we want to defer stuff if possible instead of throwing promises
|
|
14
14
|
let DEFAULT_DEFER = true;
|
|
15
15
|
export function useAsyncSub(signal, params, options) {
|
|
16
|
-
|
|
16
|
+
const normalized = normalizeUseSubArgs(signal, params, options);
|
|
17
|
+
return runUseSub(normalized.signal, normalized.params, { ...normalized.options, async: true });
|
|
17
18
|
}
|
|
18
19
|
export default function useSub(signal, params, options) {
|
|
19
|
-
|
|
20
|
+
const normalized = normalizeUseSubArgs(signal, params, options);
|
|
21
|
+
return runUseSub(normalized.signal, normalized.params, normalized.options);
|
|
22
|
+
}
|
|
23
|
+
function normalizeUseSubArgs(signal, params, options) {
|
|
24
|
+
if (options === undefined && params !== undefined && isPublicDocumentSignal(signal) && isUseSubOptions(params)) {
|
|
25
|
+
return {
|
|
26
|
+
signal,
|
|
27
|
+
params: undefined,
|
|
28
|
+
options: params
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
return { signal, params, options };
|
|
32
|
+
}
|
|
33
|
+
function isUseSubOptions(value) {
|
|
34
|
+
if (value == null || typeof value !== 'object' || Array.isArray(value))
|
|
35
|
+
return false;
|
|
36
|
+
return Object.keys(value).every(key => USE_SUB_OPTION_KEYS.has(key));
|
|
20
37
|
}
|
|
21
38
|
function runUseSub(signal, params, options) {
|
|
22
39
|
if (USE_DEFERRED_VALUE) {
|
|
@@ -27,13 +44,10 @@ function runUseSub(signal, params, options) {
|
|
|
27
44
|
}
|
|
28
45
|
}
|
|
29
46
|
// version of sub() which works as a react hook and throws promise for Suspense
|
|
30
|
-
export function useSubDeferred(signal, params, { async = false, defer, batch = false
|
|
47
|
+
export function useSubDeferred(signal, params, { async = false, defer, batch = false } = {}) {
|
|
31
48
|
const $signalRef = useRef();
|
|
32
|
-
const componentId = useId();
|
|
33
49
|
const scheduleUpdate = useScheduleUpdate();
|
|
34
50
|
const observerDefer = useDefer();
|
|
35
|
-
if (compatAttemptCleanup)
|
|
36
|
-
markCompatComponent(componentId);
|
|
37
51
|
if (batch)
|
|
38
52
|
promiseBatcher.activate();
|
|
39
53
|
defer ??= observerDefer ?? DEFAULT_DEFER;
|
|
@@ -52,8 +66,6 @@ export function useSubDeferred(signal, params, { async = false, defer, batch = f
|
|
|
52
66
|
// On resubscribe we keep rendering previous signal and refresh in background.
|
|
53
67
|
if (!hasPreviousSignal) {
|
|
54
68
|
promiseBatcher.add(promise);
|
|
55
|
-
if (compatAttemptCleanup)
|
|
56
|
-
registerCompatAttemptCleanup(signal, params);
|
|
57
69
|
}
|
|
58
70
|
else {
|
|
59
71
|
scheduleUpdate(promise);
|
|
@@ -71,8 +83,6 @@ export function useSubDeferred(signal, params, { async = false, defer, batch = f
|
|
|
71
83
|
scheduleUpdate(promise);
|
|
72
84
|
return $signalRef.current;
|
|
73
85
|
}
|
|
74
|
-
if (compatAttemptCleanup)
|
|
75
|
-
registerCompatAttemptCleanup(signal, params);
|
|
76
86
|
throw promise;
|
|
77
87
|
// 2. if it's a signal, we save it into ref to make sure it's not garbage collected while component exists
|
|
78
88
|
}
|
|
@@ -85,13 +95,10 @@ export function useSubDeferred(signal, params, { async = false, defer, batch = f
|
|
|
85
95
|
}
|
|
86
96
|
// classic version which initially throws promise for Suspense
|
|
87
97
|
// but if we get a promise second time, we return the last signal and wait for promise to resolve
|
|
88
|
-
export function useSubClassic(signal, params, { async = false, batch = false
|
|
98
|
+
export function useSubClassic(signal, params, { async = false, batch = false } = {}) {
|
|
89
99
|
const id = executionContextTracker.newHookId();
|
|
90
|
-
const componentId = useId();
|
|
91
100
|
const cache = useCache(undefined);
|
|
92
101
|
const scheduleUpdate = useScheduleUpdate();
|
|
93
|
-
if (compatAttemptCleanup)
|
|
94
|
-
markCompatComponent(componentId);
|
|
95
102
|
if (batch)
|
|
96
103
|
promiseBatcher.activate();
|
|
97
104
|
const promiseOrSignal = params != null ? runtimeSub(signal, params) : runtimeSub(signal);
|
|
@@ -104,8 +111,6 @@ export function useSubClassic(signal, params, { async = false, batch = false, co
|
|
|
104
111
|
// On resubscribe we keep rendering previous signal and refresh in background.
|
|
105
112
|
if (!hasPreviousSignal) {
|
|
106
113
|
promiseBatcher.add(promise);
|
|
107
|
-
if (compatAttemptCleanup)
|
|
108
|
-
registerCompatAttemptCleanup(signal, params);
|
|
109
114
|
}
|
|
110
115
|
else {
|
|
111
116
|
scheduleUpdate(promise);
|
|
@@ -126,8 +131,6 @@ export function useSubClassic(signal, params, { async = false, batch = false, co
|
|
|
126
131
|
scheduleUpdate(promise);
|
|
127
132
|
return;
|
|
128
133
|
}
|
|
129
|
-
if (compatAttemptCleanup)
|
|
130
|
-
registerCompatAttemptCleanup(signal, params);
|
|
131
134
|
// in regular mode we throw the promise to be caught by Suspense
|
|
132
135
|
// this way we guarantee that the signal with all the data
|
|
133
136
|
// will always be there when component is rendered
|
|
@@ -180,10 +183,3 @@ function isThenable(value) {
|
|
|
180
183
|
(typeof value === 'object' || typeof value === 'function') &&
|
|
181
184
|
typeof value.then === 'function';
|
|
182
185
|
}
|
|
183
|
-
function registerCompatAttemptCleanup(_signal, _params) {
|
|
184
|
-
// Compat hooks don't build per-hook init objects like Racer.
|
|
185
|
-
// We still need a marker so trapRender can defer observer-shell cleanup
|
|
186
|
-
// only when a real attempt cleanup exists.
|
|
187
|
-
// This path must not arm suspense-gate keep-alive by itself.
|
|
188
|
-
renderAttemptDestroyer.armCompatAttemptCleanup();
|
|
189
|
-
}
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import executionContextTracker from "./executionContextTracker.js";
|
|
2
|
-
import { useCache
|
|
3
|
-
import { markCompatComponent } from "./compatComponentRegistry.js";
|
|
2
|
+
import { useCache } from "./helpers.js";
|
|
4
3
|
import renderAttemptDestroyer from "./renderAttemptDestroyer.js";
|
|
5
4
|
const IN_FLIGHT_BY_KEY = new Map();
|
|
6
5
|
export default function useSuspendMemo(factory, deps) {
|
|
7
6
|
if (typeof factory !== 'function')
|
|
8
7
|
throw Error('useSuspendMemo() expects a factory function');
|
|
9
8
|
deps = normalizeDeps(deps);
|
|
10
|
-
const componentId = useId();
|
|
11
9
|
const cache = useCache(undefined);
|
|
12
10
|
const hookId = executionContextTracker.newHookId();
|
|
13
11
|
const cacheKey = `suspendMemo:${hookId}`;
|
|
@@ -24,7 +22,6 @@ export default function useSuspendMemo(factory, deps) {
|
|
|
24
22
|
if (entry.status === 'done')
|
|
25
23
|
return entry.value;
|
|
26
24
|
if (entry.status === 'pending') {
|
|
27
|
-
markCompatComponent(componentId);
|
|
28
25
|
renderAttemptDestroyer.armSuspenseGate();
|
|
29
26
|
throw entry.promise;
|
|
30
27
|
}
|
|
@@ -45,7 +42,6 @@ export default function useSuspendMemo(factory, deps) {
|
|
|
45
42
|
});
|
|
46
43
|
entry.status = 'pending';
|
|
47
44
|
entry.promise = promise;
|
|
48
|
-
markCompatComponent(componentId);
|
|
49
45
|
renderAttemptDestroyer.armSuspenseGate();
|
|
50
46
|
throw promise;
|
|
51
47
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "teamplay",
|
|
3
|
-
"version": "0.5.0-alpha.
|
|
3
|
+
"version": "0.5.0-alpha.15",
|
|
4
4
|
"description": "Full-stack signals ORM with multiplayer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -134,5 +134,5 @@
|
|
|
134
134
|
]
|
|
135
135
|
},
|
|
136
136
|
"license": "MIT",
|
|
137
|
-
"gitHead": "
|
|
137
|
+
"gitHead": "87701e8a3d9512a71eb2faebf01901c939cc323b"
|
|
138
138
|
}
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
export declare function markCompatComponent(componentId: string | undefined): void;
|
|
2
|
-
export declare function unmarkCompatComponent(componentId: string | undefined): void;
|
|
3
|
-
export declare function isCompatComponent(componentId: string | undefined): boolean;
|
|
4
|
-
export declare function __resetCompatComponentRegistryForTests(): void;
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
const compatComponentIds = new Set();
|
|
2
|
-
export function markCompatComponent(componentId) {
|
|
3
|
-
if (!componentId)
|
|
4
|
-
return;
|
|
5
|
-
compatComponentIds.add(componentId);
|
|
6
|
-
}
|
|
7
|
-
export function unmarkCompatComponent(componentId) {
|
|
8
|
-
if (!componentId)
|
|
9
|
-
return;
|
|
10
|
-
compatComponentIds.delete(componentId);
|
|
11
|
-
}
|
|
12
|
-
export function isCompatComponent(componentId) {
|
|
13
|
-
if (!componentId)
|
|
14
|
-
return false;
|
|
15
|
-
return compatComponentIds.has(componentId);
|
|
16
|
-
}
|
|
17
|
-
export function __resetCompatComponentRegistryForTests() {
|
|
18
|
-
compatComponentIds.clear();
|
|
19
|
-
}
|