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.
@@ -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 if (isCompatComponent(componentId)) {
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 shouldRunAttemptCleanup = this.compatAttemptCleanupArmed && this.fns.length > 0;
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
  }
@@ -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, destroyAttempt } = renderAttemptDestroyer.consumeThenableHandling();
27
+ const { shouldKeepShellAlive } = renderAttemptDestroyer.consumeThenableHandling();
28
28
  if (shouldKeepShellAlive) {
29
- throw Promise.resolve(err).then(() => destroyAttempt?.());
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)
@@ -1,5 +1,5 @@
1
1
  import type { AggregationFunction, AggregationParams, ClientAggregationFunction } from '@teamplay/utils/aggregation';
2
- import type { CollectionSignal, ComputedQueryParamsInput, DocumentSignal, QueryParams, RegisteredAggregationInput, SignalModelConstructor, SubResult, TypedAggregationInput, TypedAggregationSignal, WildcardSignalPath } from '../orm/Signal.js';
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, compatAttemptCleanup }?: UseSubOptions): unknown;
116
- export declare function useSubClassic(signal: unknown, params?: unknown, { async, batch, compatAttemptCleanup }?: UseSubOptions): unknown;
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;
@@ -1,11 +1,11 @@
1
1
  import { useRef, useDeferredValue } from 'react';
2
2
  import sub from "../orm/sub.js";
3
- import { useScheduleUpdate, useCache, useDefer, useId } from "./helpers.js";
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 renderAttemptDestroyer from "./renderAttemptDestroyer.js";
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
- return runUseSub(signal, params, { ...options, async: true });
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
- return runUseSub(signal, params, options);
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, compatAttemptCleanup = 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, compatAttemptCleanup = 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, useId } from "./helpers.js";
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.13",
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": "3b067b76508f175ddc12bff294f76682796185f8"
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
- }