teamplay 0.5.0-alpha.24 → 0.5.0-alpha.26

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/index.d.ts CHANGED
@@ -53,7 +53,8 @@ export { default as signal } from './orm/getSignal.js';
53
53
  export { GLOBAL_ROOT_ID } from './orm/Root.js';
54
54
  export declare const $: RootSignal;
55
55
  export default $;
56
- export { default as sub } from './orm/sub.js';
56
+ export { default as sub, unsub } from './orm/sub.js';
57
+ export type { SubMode, SubOptions } from './orm/sub.js';
57
58
  export { default as useSub, useAsyncSub, useBatchSub, setUseDeferredValue as __setUseDeferredValue, setDefaultDefer as __setDefaultDefer } from './react/useSub.js';
58
59
  export { default as useSuspendMemo, useSuspendMemoByKey } from './react/useSuspendMemo.js';
59
60
  export declare const observer: ObserverFunction;
package/dist/index.js CHANGED
@@ -14,7 +14,7 @@ export { GLOBAL_ROOT_ID } from "./orm/Root.js";
14
14
  const getRuntimeRootSignal = _getRootSignal;
15
15
  export const $ = getRuntimeRootSignal({ rootId: GLOBAL_ROOT_ID, rootFunction: universal$ });
16
16
  export default $;
17
- export { default as sub } from "./orm/sub.js";
17
+ export { default as sub, unsub } from "./orm/sub.js";
18
18
  export { default as useSub, useAsyncSub, useBatchSub, setUseDeferredValue as __setUseDeferredValue, setDefaultDefer as __setDefaultDefer } from "./react/useSub.js";
19
19
  export { default as useSuspendMemo, useSuspendMemoByKey } from "./react/useSuspendMemo.js";
20
20
  export const observer = runtimeObserver;
@@ -178,14 +178,14 @@ export async function setPublicDoc(segments, value, deleteValue = false) {
178
178
  if (isIdFieldPath(segments, idFields))
179
179
  return;
180
180
  const doc = getConnection().get(collection, docId);
181
- let docState = resolvePublicDocState({ collection, docId, doc, idFields, hydrateCompatDocData: true });
181
+ let docState = resolvePublicDocState({ collection, docId, doc, idFields, hydrateDocDataFromLocal: true });
182
182
  if (!docState.exists && segments.length > 2) {
183
- docState = await resolvePublicDocStateWithCompatFetchFallback({
183
+ docState = await resolvePublicDocStateWithFetchFallback({
184
184
  collection,
185
185
  docId,
186
186
  doc,
187
187
  idFields,
188
- hydrateCompatDocData: true
188
+ hydrateDocDataFromLocal: true
189
189
  });
190
190
  }
191
191
  if (!docState.exists && deleteValue)
@@ -286,14 +286,14 @@ export async function setPublicDocReplace(segments, value) {
286
286
  if (isIdFieldPath(segments, idFields))
287
287
  return;
288
288
  const doc = getConnection().get(collection, docId);
289
- let docState = resolvePublicDocState({ collection, docId, doc, idFields, hydrateCompatDocData: true });
289
+ let docState = resolvePublicDocState({ collection, docId, doc, idFields, hydrateDocDataFromLocal: true });
290
290
  if (!docState.exists && segments.length > 2) {
291
- docState = await resolvePublicDocStateWithCompatFetchFallback({
291
+ docState = await resolvePublicDocStateWithFetchFallback({
292
292
  collection,
293
293
  docId,
294
294
  doc,
295
295
  idFields,
296
- hydrateCompatDocData: true
296
+ hydrateDocDataFromLocal: true
297
297
  });
298
298
  }
299
299
  // make sure that the value is not observable to not trigger extra reads. And clone it
@@ -374,9 +374,10 @@ async function createPublicDocAndHydrateLocal({ doc, collection, docId, newDoc,
374
374
  await new Promise((resolve, reject) => {
375
375
  doc.create(newDoc, err => err ? reject(err) : resolve());
376
376
  });
377
- // In compatibility mode we must allow immediate subpath writes after create()
378
- // even when the ShareDB snapshot hasn't been loaded via subscribe/fetch yet.
379
- if (isCompatEnv() && doc?.data == null) {
377
+ // Keep public creates immediately writable even when the ShareDB snapshot has
378
+ // not been loaded via subscribe/fetch yet. UI flows rely on optimistic add()
379
+ // followed by child writes without waiting for DB confirmation.
380
+ if (doc?.data == null) {
380
381
  const localDoc = JSON.parse(JSON.stringify(newDoc || {}));
381
382
  if (isPlainObject(localDoc))
382
383
  injectIdFields(localDoc, idFields, docId);
@@ -387,7 +388,7 @@ async function createPublicDocAndHydrateLocal({ doc, collection, docId, newDoc,
387
388
  }
388
389
  ensureLocalDocSyncedWithShareDoc({ collection, docId, doc, idFields });
389
390
  }
390
- function resolvePublicDocState({ collection, docId, doc, idFields, hydrateCompatDocData = false }) {
391
+ function resolvePublicDocState({ collection, docId, doc, idFields, hydrateDocDataFromLocal = false }) {
391
392
  ensureLocalDocSyncedWithShareDoc({ collection, docId, doc, idFields });
392
393
  if (isMissingShareDoc(doc)) {
393
394
  return { exists: false, snapshot: undefined, source: 'none' };
@@ -400,24 +401,29 @@ function resolvePublicDocState({ collection, docId, doc, idFields, hydrateCompat
400
401
  };
401
402
  }
402
403
  const localSnapshot = getRaw([collection, docId]);
403
- if (!(isCompatEnv() && localSnapshot != null)) {
404
+ if (localSnapshot == null) {
404
405
  return { exists: false, snapshot: undefined, source: 'none' };
405
406
  }
406
- // In compat mode local raw data can be the source of truth between create/add
407
- // and later subpath mutations even if ShareDB doc.data is currently empty.
408
- if (hydrateCompatDocData) {
407
+ // Local raw data can be the source of truth between add() and later subpath
408
+ // mutations even if ShareDB doc.data is currently empty or was recreated.
409
+ if (hydrateDocDataFromLocal) {
409
410
  doc.data = localSnapshot;
410
411
  }
411
412
  return { exists: true, snapshot: localSnapshot, source: 'local' };
412
413
  }
413
- async function resolvePublicDocStateWithCompatFetchFallback({ collection, docId, doc, idFields, hydrateCompatDocData = false }) {
414
- let docState = resolvePublicDocState({ collection, docId, doc, idFields, hydrateCompatDocData });
415
- if (docState.exists || !isCompatEnv())
414
+ async function resolvePublicDocStateWithFetchFallback({ collection, docId, doc, idFields, hydrateDocDataFromLocal = false }) {
415
+ let docState = resolvePublicDocState({ collection, docId, doc, idFields, hydrateDocDataFromLocal });
416
+ if (docState.exists)
417
+ return docState;
418
+ const shouldFetch = isCompatEnv() || (getRaw([collection, docId]) != null &&
419
+ isMissingShareDoc(doc) &&
420
+ doc.version == null);
421
+ if (!shouldFetch)
416
422
  return docState;
417
423
  await new Promise((resolve, reject) => {
418
424
  doc.fetch(err => err ? reject(err) : resolve());
419
425
  });
420
- docState = resolvePublicDocState({ collection, docId, doc, idFields, hydrateCompatDocData });
426
+ docState = resolvePublicDocState({ collection, docId, doc, idFields, hydrateDocDataFromLocal });
421
427
  return docState;
422
428
  }
423
429
  function ensureLocalDocSyncedWithShareDoc({ collection, docId, doc, idFields }) {
@@ -572,12 +578,12 @@ export async function incrementPublic(segments, byNumber) {
572
578
  throw Error(ERRORS.publicDoc(segments));
573
579
  const doc = getConnection().get(collection, docId);
574
580
  const idFields = getIdFieldsForSegments([collection, docId]);
575
- const docState = await resolvePublicDocStateWithCompatFetchFallback({
581
+ const docState = await resolvePublicDocStateWithFetchFallback({
576
582
  collection,
577
583
  docId,
578
584
  doc,
579
585
  idFields,
580
- hydrateCompatDocData: true
586
+ hydrateDocDataFromLocal: true
581
587
  });
582
588
  if (!docState.exists)
583
589
  throw Error(ERRORS.nonExistingDoc(segments));
@@ -615,12 +621,12 @@ export async function arrayInsertPublic(segments, index, values) {
615
621
  throw Error(ERRORS.publicDoc(segments));
616
622
  const doc = getConnection().get(collection, docId);
617
623
  const idFields = getIdFieldsForSegments([collection, docId]);
618
- const docState = await resolvePublicDocStateWithCompatFetchFallback({
624
+ const docState = await resolvePublicDocStateWithFetchFallback({
619
625
  collection,
620
626
  docId,
621
627
  doc,
622
628
  idFields,
623
- hydrateCompatDocData: true
629
+ hydrateDocDataFromLocal: true
624
630
  });
625
631
  if (!docState.exists)
626
632
  throw Error(ERRORS.nonExistingDoc(segments));
@@ -674,12 +680,12 @@ export async function arrayRemovePublic(segments, index, howMany = 1) {
674
680
  throw Error(ERRORS.publicDoc(segments));
675
681
  const doc = getConnection().get(collection, docId);
676
682
  const idFields = getIdFieldsForSegments([collection, docId]);
677
- const docState = await resolvePublicDocStateWithCompatFetchFallback({
683
+ const docState = await resolvePublicDocStateWithFetchFallback({
678
684
  collection,
679
685
  docId,
680
686
  doc,
681
687
  idFields,
682
- hydrateCompatDocData: true
688
+ hydrateDocDataFromLocal: true
683
689
  });
684
690
  if (!docState.exists)
685
691
  throw Error(ERRORS.nonExistingDoc(segments));
@@ -702,12 +708,12 @@ export async function arrayMovePublic(segments, from, to, howMany = 1) {
702
708
  throw Error(ERRORS.publicDoc(segments));
703
709
  const doc = getConnection().get(collection, docId);
704
710
  const idFields = getIdFieldsForSegments([collection, docId]);
705
- const docState = await resolvePublicDocStateWithCompatFetchFallback({
711
+ const docState = await resolvePublicDocStateWithFetchFallback({
706
712
  collection,
707
713
  docId,
708
714
  doc,
709
715
  idFields,
710
- hydrateCompatDocData: true
716
+ hydrateDocDataFromLocal: true
711
717
  });
712
718
  if (!docState.exists)
713
719
  throw Error(ERRORS.nonExistingDoc(segments));
@@ -780,12 +786,12 @@ export async function stringInsertPublic(segments, index, text) {
780
786
  throw Error(ERRORS.publicDoc(segments));
781
787
  const doc = getConnection().get(collection, docId);
782
788
  const idFields = getIdFieldsForSegments([collection, docId]);
783
- const docState = await resolvePublicDocStateWithCompatFetchFallback({
789
+ const docState = await resolvePublicDocStateWithFetchFallback({
784
790
  collection,
785
791
  docId,
786
792
  doc,
787
793
  idFields,
788
- hydrateCompatDocData: true
794
+ hydrateDocDataFromLocal: true
789
795
  });
790
796
  if (!docState.exists)
791
797
  throw Error(ERRORS.nonExistingDoc(segments));
@@ -814,12 +820,12 @@ export async function stringRemovePublic(segments, index, howMany) {
814
820
  throw Error(ERRORS.publicDoc(segments));
815
821
  const doc = getConnection().get(collection, docId);
816
822
  const idFields = getIdFieldsForSegments([collection, docId]);
817
- const docState = await resolvePublicDocStateWithCompatFetchFallback({
823
+ const docState = await resolvePublicDocStateWithFetchFallback({
818
824
  collection,
819
825
  docId,
820
826
  doc,
821
827
  idFields,
822
- hydrateCompatDocData: true
828
+ hydrateDocDataFromLocal: true
823
829
  });
824
830
  if (!docState.exists)
825
831
  throw Error(ERRORS.nonExistingDoc(segments));
package/dist/orm/sub.d.ts CHANGED
@@ -1,40 +1,44 @@
1
1
  import type { AggregationFunction, AggregationParams, ClientAggregationFunction } from '@teamplay/utils/aggregation';
2
2
  import type { CollectionSignal, ComputedQueryParamsInput, DocumentSignal, MaybePromise, MaybePromiseSubResult, QueryParams, RegisteredAggregationInput, SignalModelConstructor, SubResult, TypedAggregationInput, TypedAggregationSignal, WildcardSignalPath } from './Signal.js';
3
+ export type SubMode = 'auto' | 'fetch' | 'subscribe';
4
+ export interface SubOptions {
5
+ mode?: SubMode;
6
+ }
3
7
  /**
4
8
  * Subscribe to an aggregation with explicit output typing outside React.
5
9
  * @param $aggregation Typed aggregation input.
6
10
  * @param params Parameters passed to the aggregation.
7
11
  */
8
- export default function sub<TDocument, TDocumentModel extends SignalModelConstructor<TDocument>>($aggregation: TypedAggregationInput<TDocument, TDocumentModel>, params?: AggregationParams): MaybePromise<TypedAggregationSignal<TDocument, TDocumentModel>>;
12
+ export default function sub<TDocument, TDocumentModel extends SignalModelConstructor<TDocument>>($aggregation: TypedAggregationInput<TDocument, TDocumentModel>, params?: AggregationParams, options?: SubOptions): MaybePromise<TypedAggregationSignal<TDocument, TDocumentModel>>;
9
13
  /**
10
14
  * Subscribe to a registered collection aggregation outside React.
11
15
  * @param $aggregation Aggregation header generated by StartupJS model loading.
12
16
  * @param params Parameters passed to the aggregation.
13
17
  */
14
- export default function sub<TCollection extends string, TOutput = unknown>($aggregation: RegisteredAggregationInput<TCollection, TOutput>, params?: AggregationParams): MaybePromise<SubResult<RegisteredAggregationInput<TCollection, TOutput>>>;
18
+ export default function sub<TCollection extends string, TOutput = unknown>($aggregation: RegisteredAggregationInput<TCollection, TOutput>, params?: AggregationParams, options?: SubOptions): MaybePromise<SubResult<RegisteredAggregationInput<TCollection, TOutput>>>;
15
19
  /**
16
20
  * Subscribe to a client aggregation outside React.
17
21
  * @param $aggregation Aggregation function created with `aggregation(collection, fn)`.
18
22
  * @param params Parameters passed to the aggregation.
19
23
  */
20
- export default function sub<TOutput, TCollection extends string>($aggregation: ClientAggregationFunction<TOutput, TCollection>, params?: AggregationParams): MaybePromise<SubResult<ClientAggregationFunction<TOutput, TCollection>>>;
24
+ export default function sub<TOutput, TCollection extends string>($aggregation: ClientAggregationFunction<TOutput, TCollection>, params?: AggregationParams, options?: SubOptions): MaybePromise<SubResult<ClientAggregationFunction<TOutput, TCollection>>>;
21
25
  /**
22
26
  * Subscribe to an unregistered aggregation outside React.
23
27
  * @param $aggregation Aggregation function.
24
28
  * @param params Parameters passed to the aggregation.
25
29
  */
26
- export default function sub<TOutput = unknown, TCollection extends string = string>($aggregation: AggregationFunction<TOutput, TCollection>, params?: AggregationParams): MaybePromise<SubResult<AggregationFunction<TOutput, TCollection>>>;
30
+ export default function sub<TOutput = unknown, TCollection extends string = string>($aggregation: AggregationFunction<TOutput, TCollection>, params?: AggregationParams, options?: SubOptions): MaybePromise<SubResult<AggregationFunction<TOutput, TCollection>>>;
27
31
  /**
28
32
  * Subscribe to a document signal outside React and return it when ready.
29
33
  * @param $signal Document signal to subscribe to.
30
34
  */
31
- export default function sub<TSignal extends DocumentSignal<any, any, any>>($signal: TSignal): MaybePromiseSubResult<TSignal>;
35
+ export default function sub<TSignal extends DocumentSignal<any, any, any>>($signal: TSignal, options?: SubOptions): MaybePromiseSubResult<TSignal>;
32
36
  /**
33
37
  * Subscribe to a collection query outside React and return a query signal when ready.
34
38
  * @param $collection Collection signal to query.
35
39
  * @param params Mongo-style query params, including filters and `$sort`.
36
40
  */
37
- export default function sub<TDocument, TCollectionModel extends SignalModelConstructor<TDocument[]>, TDocumentModel extends SignalModelConstructor<TDocument>, TCollectionPath extends WildcardSignalPath>($collection: CollectionSignal<TDocument, TCollectionModel, TDocumentModel, TCollectionPath>, params: QueryParams<TDocument>): MaybePromiseSubResult<CollectionSignal<TDocument, TCollectionModel, TDocumentModel, TCollectionPath>, QueryParams<TDocument>>;
41
+ export default function sub<TDocument, TCollectionModel extends SignalModelConstructor<TDocument[]>, TDocumentModel extends SignalModelConstructor<TDocument>, TCollectionPath extends WildcardSignalPath>($collection: CollectionSignal<TDocument, TCollectionModel, TDocumentModel, TCollectionPath>, params: QueryParams<TDocument>, options?: SubOptions): MaybePromiseSubResult<CollectionSignal<TDocument, TCollectionModel, TDocumentModel, TCollectionPath>, QueryParams<TDocument>>;
38
42
  /**
39
43
  * Subscribe to a collection query with computed string keys outside React.
40
44
  * This fallback preserves Mongo-style computed paths such as `{ [`likes.${id}`]: true }`.
@@ -42,4 +46,5 @@ export default function sub<TDocument, TCollectionModel extends SignalModelConst
42
46
  * @param $collection Collection signal to query.
43
47
  * @param params Mongo-style query params with a widened computed key.
44
48
  */
45
- export default function sub<TDocument, TCollectionModel extends SignalModelConstructor<TDocument[]>, TDocumentModel extends SignalModelConstructor<TDocument>, TCollectionPath extends WildcardSignalPath, TParams extends object>($collection: CollectionSignal<TDocument, TCollectionModel, TDocumentModel, TCollectionPath>, params: TParams & ComputedQueryParamsInput<TParams>): MaybePromiseSubResult<CollectionSignal<TDocument, TCollectionModel, TDocumentModel, TCollectionPath>, TParams>;
49
+ export default function sub<TDocument, TCollectionModel extends SignalModelConstructor<TDocument[]>, TDocumentModel extends SignalModelConstructor<TDocument>, TCollectionPath extends WildcardSignalPath, TParams extends object>($collection: CollectionSignal<TDocument, TCollectionModel, TDocumentModel, TCollectionPath>, params: TParams & ComputedQueryParamsInput<TParams>, options?: SubOptions): MaybePromiseSubResult<CollectionSignal<TDocument, TCollectionModel, TDocumentModel, TCollectionPath>, TParams>;
50
+ export declare function unsub($signal: unknown): Promise<void> | void;
package/dist/orm/sub.js CHANGED
@@ -3,9 +3,11 @@ import Signal, { SEGMENTS, isPublicCollectionSignal, isPublicDocumentSignal } fr
3
3
  import { docSubscriptions } from './Doc.js';
4
4
  import { querySubscriptions, getQuerySignal } from './Query.js';
5
5
  import { aggregationSubscriptions, getAggregationSignal } from './Aggregation.js';
6
- import { getRoot } from "./Root.js";
6
+ import { getRoot, ROOT_ID } from "./Root.js";
7
+ import { isRootContextClosed } from "./rootContext.js";
7
8
  import isServer from "../utils/isServer.js";
8
- export default function sub($signal, params) {
9
+ const SUB_RECORDS = new WeakMap();
10
+ export default function sub($signal, params, options) {
9
11
  // TODO: temporarily disable support for multiple subscriptions
10
12
  // since this has to be properly cached using useDeferredSignal() in useSub()
11
13
  // if (Array.isArray($signal)) {
@@ -16,26 +18,26 @@ export default function sub($signal, params) {
16
18
  if (Array.isArray($signal))
17
19
  throw Error('sub() does not support multiple subscriptions yet');
18
20
  if (isRuntimePublicDocumentSignal($signal)) {
19
- if (arguments.length > 1) {
21
+ if (arguments.length > 2) {
20
22
  throw Error(ERRORS.subDocArguments($signal, ...Array.from(arguments).slice(1)));
21
23
  }
22
- return doc$($signal);
24
+ return doc$($signal, parseSubOptions(params));
23
25
  }
24
26
  else if (isRuntimePublicCollectionSignal($signal)) {
25
- if (arguments.length !== 2) {
27
+ if (arguments.length < 2 || arguments.length > 3) {
26
28
  throw Error(ERRORS.subQueryArguments($signal, params, ...Array.from(arguments).slice(2)));
27
29
  }
28
- return query$($signal, params);
30
+ return query$($signal, params, parseSubOptions(options));
29
31
  }
30
32
  else if (isClientAggregationFunction($signal)) {
31
- return getAggregationFromFunction($signal, $signal.collection, params);
33
+ return getAggregationFromFunction($signal, $signal.collection, params, parseSubOptions(options));
32
34
  }
33
35
  else if (isAggregationHeader($signal)) {
34
36
  const aggregationParams = {
35
37
  $aggregationName: $signal.name,
36
38
  $params: sanitizeAggregationParams(params)
37
39
  };
38
- return aggregation$($signal.collection, aggregationParams);
40
+ return aggregation$($signal.collection, aggregationParams, undefined, parseSubOptions(options));
39
41
  }
40
42
  else if (isAggregationFunction($signal)) {
41
43
  if (isServer) {
@@ -46,7 +48,7 @@ export default function sub($signal, params) {
46
48
  }
47
49
  const aggregationParams = { ...serverParams };
48
50
  delete aggregationParams.$collection;
49
- return getAggregationFromFunction($signal, collection, aggregationParams);
51
+ return getAggregationFromFunction($signal, collection, aggregationParams, parseSubOptions(options));
50
52
  }
51
53
  else {
52
54
  throw Error(ERRORS.gotAggregationFunction($signal));
@@ -59,7 +61,25 @@ export default function sub($signal, params) {
59
61
  throw Error('Invalid args passed for sub()');
60
62
  }
61
63
  }
62
- function getAggregationFromFunction(fn, collection, params) {
64
+ export function unsub($signal) {
65
+ if (!($signal instanceof Signal))
66
+ return;
67
+ const record = takeSubRecord($signal);
68
+ if (!record)
69
+ return;
70
+ record.disposed = true;
71
+ const $root = getRoot($signal);
72
+ const rootId = $root?.[ROOT_ID];
73
+ if (isRootContextClosed(rootId))
74
+ return;
75
+ if (record.kind === 'doc')
76
+ return docSubscriptions.unsubscribe($signal, { intent: record.intent });
77
+ if (record.kind === 'query')
78
+ return querySubscriptions.unsubscribe($signal, { intent: record.intent });
79
+ if (record.kind === 'aggregation')
80
+ return aggregationSubscriptions.unsubscribe($signal, { intent: record.intent });
81
+ }
82
+ function getAggregationFromFunction(fn, collection, params, subOptions) {
63
83
  const aggregationParams = sanitizeAggregationParams(params);
64
84
  let session;
65
85
  if (isRecord(aggregationParams) && aggregationParams.$session) {
@@ -70,15 +90,13 @@ function getAggregationFromFunction(fn, collection, params) {
70
90
  // should match the context in @teamplay/backend/features/serverAggregate.js
71
91
  const context = { collection, session, isServer };
72
92
  const result = fn(aggregationParams, context);
73
- return aggregation$(collection, Array.isArray(result) ? { $aggregate: result } : result);
93
+ return aggregation$(collection, Array.isArray(result) ? { $aggregate: result } : result, undefined, subOptions);
74
94
  }
75
- function doc$($doc) {
76
- const promise = docSubscriptions.subscribe($doc);
77
- if (!promise)
78
- return $doc;
79
- return new Promise(resolve => promise.then(() => resolve($doc)));
95
+ function doc$($doc, subOptions) {
96
+ const promise = docSubscriptions.subscribe($doc, { intent: subOptions.intent });
97
+ return returnSubscribedSignal($doc, 'doc', subOptions.intent, promise);
80
98
  }
81
- function query$($collection, params) {
99
+ function query$($collection, params, subOptions) {
82
100
  const collectionName = $collection[SEGMENTS][0];
83
101
  if (typeof collectionName !== 'string')
84
102
  throw Error(ERRORS.queryCollectionName($collection));
@@ -86,20 +104,17 @@ function query$($collection, params) {
86
104
  throw Error(ERRORS.queryParamsObject(collectionName, params));
87
105
  const signalOptions = getQuerySignalOptions($collection);
88
106
  const queryParams = params;
89
- if (queryParams?.$aggregate || queryParams?.$aggregationName)
90
- return aggregation$(collectionName, params, signalOptions);
107
+ if (queryParams?.$aggregate || queryParams?.$aggregationName) {
108
+ return aggregation$(collectionName, params, signalOptions, subOptions);
109
+ }
91
110
  const $query = getQuerySignal(collectionName, params, signalOptions);
92
- const promise = querySubscriptions.subscribe($query);
93
- if (!promise)
94
- return $query;
95
- return new Promise(resolve => promise.then(() => resolve($query)));
111
+ const promise = querySubscriptions.subscribe($query, { intent: subOptions.intent });
112
+ return returnSubscribedSignal($query, 'query', subOptions.intent, promise);
96
113
  }
97
- function aggregation$(collectionName, params, signalOptions) {
114
+ function aggregation$(collectionName, params, signalOptions, subOptions = parseSubOptions()) {
98
115
  const $aggregationQuery = getAggregationSignal(collectionName, params, signalOptions);
99
- const promise = aggregationSubscriptions.subscribe($aggregationQuery);
100
- if (!promise)
101
- return $aggregationQuery;
102
- return new Promise(resolve => promise.then(() => resolve($aggregationQuery)));
116
+ const promise = aggregationSubscriptions.subscribe($aggregationQuery, { intent: subOptions.intent });
117
+ return returnSubscribedSignal($aggregationQuery, 'aggregation', subOptions.intent, promise);
103
118
  }
104
119
  function api$(_fn, _args) {
105
120
  throw Error('sub() for async functions is not implemented yet');
@@ -133,14 +148,56 @@ function isRuntimePublicCollectionSignal(value) {
133
148
  function isApiFunction(value) {
134
149
  return typeof value === 'function' && !(value instanceof Signal);
135
150
  }
151
+ function parseSubOptions(options) {
152
+ const mode = options?.mode ?? 'auto';
153
+ if (mode !== 'auto' && mode !== 'fetch' && mode !== 'subscribe') {
154
+ throw Error(`sub() option mode must be "auto", "fetch", or "subscribe". Got: ${mode}`);
155
+ }
156
+ return {
157
+ intent: mode === 'fetch' ? 'fetch' : 'subscribe'
158
+ };
159
+ }
160
+ function returnSubscribedSignal($signal, kind, intent, promise) {
161
+ if (!promise) {
162
+ addSubRecord($signal, kind, intent);
163
+ return $signal;
164
+ }
165
+ return promise.then(() => {
166
+ addSubRecord($signal, kind, intent);
167
+ return $signal;
168
+ });
169
+ }
170
+ function addSubRecord($signal, kind, intent) {
171
+ let records = SUB_RECORDS.get($signal);
172
+ if (!records) {
173
+ records = [];
174
+ SUB_RECORDS.set($signal, records);
175
+ }
176
+ records.push({ kind, intent, disposed: false });
177
+ }
178
+ function takeSubRecord($signal) {
179
+ const records = SUB_RECORDS.get($signal);
180
+ if (!records)
181
+ return;
182
+ for (let i = records.length - 1; i >= 0; i--) {
183
+ const record = records[i];
184
+ if (record.disposed)
185
+ continue;
186
+ records.splice(i, 1);
187
+ if (records.length === 0)
188
+ SUB_RECORDS.delete($signal);
189
+ return record;
190
+ }
191
+ SUB_RECORDS.delete($signal);
192
+ }
136
193
  const ERRORS = {
137
194
  subDocArguments: ($signal, ...args) => `
138
- sub($doc) accepts only 1 argument - the document signal to subscribe to
195
+ sub($doc) accepts one or two arguments - the document signal and optional options.
139
196
  Doc: ${$signal[SEGMENTS]}
140
197
  Got args: ${[$signal, ...args]}
141
198
  `,
142
199
  subQueryArguments: ($signal, params, ...args) => `
143
- sub($collection, params) accepts 2 arguments - the collection signal and an object with query params.
200
+ sub($collection, params) accepts two or three arguments - the collection signal, query params, and optional options.
144
201
  If you want to subscribe to all documents in a collection, pass an empty object: sub($collection, {}).
145
202
  Collection: ${$signal[SEGMENTS]}
146
203
  Params: ${params}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamplay",
3
- "version": "0.5.0-alpha.24",
3
+ "version": "0.5.0-alpha.26",
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": "cdb8848af9b1f3ecc77e990a3fbc4d55d4aa0dec"
137
+ "gitHead": "a432e1c1dc272cfe2b87e3820557db4721a0f38b"
138
138
  }