teamplay 0.5.0-alpha.8 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +3 -3
  2. package/dist/config.d.ts +2 -0
  3. package/dist/config.js +1 -0
  4. package/dist/connect/index.d.ts +1 -1
  5. package/dist/connect/index.js +6 -2
  6. package/dist/connect/offline/index.d.ts +1 -1
  7. package/dist/connect/offline/index.js +8 -4
  8. package/dist/connect/offline/react-native.d.ts +1 -1
  9. package/dist/connect/offline/web.d.ts +1 -1
  10. package/dist/connect/test.d.ts +1 -1
  11. package/dist/connect/test.js +5 -1
  12. package/dist/index.d.ts +9 -5
  13. package/dist/index.js +6 -5
  14. package/dist/orm/$.js +1 -1
  15. package/dist/orm/Aggregation.d.ts +5 -3
  16. package/dist/orm/Aggregation.js +39 -15
  17. package/dist/orm/Doc.js +0 -55
  18. package/dist/orm/Query.d.ts +1 -0
  19. package/dist/orm/Query.js +25 -82
  20. package/dist/orm/Root.d.ts +4 -0
  21. package/dist/orm/Root.js +16 -0
  22. package/dist/orm/Signal.d.ts +0 -2
  23. package/dist/orm/Signal.js +1 -4
  24. package/dist/orm/SignalBase.d.ts +21 -1
  25. package/dist/orm/SignalBase.js +259 -56
  26. package/dist/orm/batchScheduler.d.ts +7 -7
  27. package/dist/orm/connection.d.ts +0 -4
  28. package/dist/orm/connection.js +0 -12
  29. package/dist/orm/dataTree.d.ts +12 -12
  30. package/dist/orm/dataTree.js +55 -107
  31. package/dist/orm/disposeRootContext.js +0 -14
  32. package/dist/orm/events.d.ts +6 -0
  33. package/dist/orm/events.js +48 -0
  34. package/dist/orm/getSignal.d.ts +1 -1
  35. package/dist/orm/getSignal.js +4 -33
  36. package/dist/orm/idFields.d.ts +10 -1
  37. package/dist/orm/idFields.js +102 -14
  38. package/dist/orm/index.d.ts +2 -0
  39. package/dist/orm/index.js +1 -0
  40. package/dist/orm/initModels.js +1 -1
  41. package/dist/orm/privateData.d.ts +7 -22
  42. package/dist/orm/privateData.js +20 -1
  43. package/dist/orm/queryReadiness.d.ts +13 -0
  44. package/dist/orm/{Compat/queryReadiness.js → queryReadiness.js} +10 -10
  45. package/dist/orm/reaction.d.ts +11 -0
  46. package/dist/orm/reaction.js +47 -0
  47. package/dist/orm/rootContext.d.ts +0 -16
  48. package/dist/orm/rootContext.js +0 -28
  49. package/dist/orm/signalMetadata.js +3 -3
  50. package/dist/orm/signalReads.js +3 -9
  51. package/dist/orm/signalStorageMutations.d.ts +0 -2
  52. package/dist/orm/signalStorageMutations.js +0 -9
  53. package/dist/orm/signalSymbols.js +1 -1
  54. package/dist/orm/signalValueMutations.d.ts +1 -1
  55. package/dist/orm/signalValueMutations.js +0 -3
  56. package/dist/orm/sub.d.ts +12 -7
  57. package/dist/orm/sub.js +87 -30
  58. package/dist/orm/subscriptionGcDelay.js +2 -6
  59. package/dist/react/convertToObserver.js +1 -4
  60. package/dist/react/promiseBatcher.js +1 -1
  61. package/dist/react/renderAttemptDestroyer.d.ts +0 -8
  62. package/dist/react/renderAttemptDestroyer.js +2 -28
  63. package/dist/react/trapRender.js +3 -3
  64. package/dist/react/useSub.d.ts +86 -5
  65. package/dist/react/useSub.js +191 -32
  66. package/dist/react/useSuspendMemo.js +1 -5
  67. package/dist/server.d.ts +2 -3
  68. package/dist/server.js +5 -3
  69. package/package.json +16 -14
  70. package/dist/orm/Compat/SignalCompat.d.ts +0 -3
  71. package/dist/orm/Compat/SignalCompat.js +0 -1267
  72. package/dist/orm/Compat/eventsCompat.d.ts +0 -3
  73. package/dist/orm/Compat/eventsCompat.js +0 -73
  74. package/dist/orm/Compat/hooksCompat.d.ts +0 -33
  75. package/dist/orm/Compat/hooksCompat.js +0 -360
  76. package/dist/orm/Compat/modelEvents.d.ts +0 -6
  77. package/dist/orm/Compat/modelEvents.js +0 -228
  78. package/dist/orm/Compat/queryReadiness.d.ts +0 -5
  79. package/dist/orm/Compat/refFallback.d.ts +0 -13
  80. package/dist/orm/Compat/refFallback.js +0 -65
  81. package/dist/orm/Compat/refRegistry.d.ts +0 -6
  82. package/dist/orm/Compat/refRegistry.js +0 -54
  83. package/dist/orm/Compat/silentContext.d.ts +0 -5
  84. package/dist/orm/Compat/silentContext.js +0 -48
  85. package/dist/orm/Compat/startStopCompat.d.ts +0 -3
  86. package/dist/orm/Compat/startStopCompat.js +0 -217
  87. package/dist/orm/compatEnv.d.ts +0 -1
  88. package/dist/orm/compatEnv.js +0 -4
  89. package/dist/react/compatComponentRegistry.d.ts +0 -4
  90. package/dist/react/compatComponentRegistry.js +0 -19
  91. /package/dist/orm/{Reaction.d.ts → reactionSubscriptions.d.ts} +0 -0
  92. /package/dist/orm/{Reaction.js → reactionSubscriptions.js} +0 -0
@@ -1,8 +1,42 @@
1
1
  import { findModel } from "./addModel.js";
2
2
  export const DEFAULT_ID_FIELDS = ['_id'];
3
+ export const TEAMPLAY_RUNTIME_CONFIG_SYMBOL = Symbol.for('teamplay.runtimeConfig');
3
4
  export function getIdFieldsForSegments(segments) {
4
5
  const Model = findModel(segments);
5
- return Model?.ID_FIELDS || DEFAULT_ID_FIELDS;
6
+ return Model?.ID_FIELDS || getDefaultIdFields();
7
+ }
8
+ export function configureTeamplay({ idFields } = {}) {
9
+ if (arguments.length === 0)
10
+ return;
11
+ const config = getGlobalRuntimeConfig(true);
12
+ const options = (arguments[0] || {});
13
+ if (Object.prototype.hasOwnProperty.call(options, 'idFields')) {
14
+ config.idFields = idFields == null
15
+ ? DEFAULT_ID_FIELDS
16
+ : normalizeIdFieldsConfig(idFields);
17
+ }
18
+ }
19
+ export function getTeamplayConfig() {
20
+ return {
21
+ idFields: getDefaultIdFields()
22
+ };
23
+ }
24
+ export function getDefaultIdFields() {
25
+ const config = getGlobalRuntimeConfig(false);
26
+ if (!config || !Object.prototype.hasOwnProperty.call(config, 'idFields')) {
27
+ return DEFAULT_ID_FIELDS;
28
+ }
29
+ if (config.idFields == null)
30
+ return DEFAULT_ID_FIELDS;
31
+ const normalized = normalizeIdFieldsConfig(config.idFields);
32
+ config.idFields = normalized;
33
+ return normalized;
34
+ }
35
+ export function setDefaultIdFields(idFields = DEFAULT_ID_FIELDS) {
36
+ getGlobalRuntimeConfig(true).idFields = normalizeIdFieldsConfig(idFields);
37
+ }
38
+ export function __resetTeamplayConfigForTests() {
39
+ delete getGlobalRuntimeConfigHolder()[TEAMPLAY_RUNTIME_CONFIG_SYMBOL];
6
40
  }
7
41
  export function isPlainObject(value) {
8
42
  if (!value || typeof value !== 'object')
@@ -52,26 +86,28 @@ export function stripIdFields(value, idFields) {
52
86
  }
53
87
  return next;
54
88
  }
55
- export function resolveAddDocId(value, getDefaultId) {
89
+ export function resolveAddDocId(value, idFields, getDefaultId) {
56
90
  if (!value || typeof value !== 'object')
57
91
  throw Error('Signal.add() expects an object argument');
58
92
  const payload = value;
59
- const hasId = payload.id != null;
60
- const hasUnderscoreId = payload._id != null;
61
- if (hasId && hasUnderscoreId && payload.id !== payload._id) {
62
- throw Error(`Signal.add() got conflicting "id" (${JSON.stringify(payload.id)}) and "_id" (${JSON.stringify(payload._id)})`);
93
+ const entries = getAddIdEntries(payload, idFields);
94
+ const [firstEntry] = entries;
95
+ const conflictEntry = firstEntry && entries.find(entry => entry.value !== firstEntry.value);
96
+ if (firstEntry && conflictEntry) {
97
+ throw Error(`Signal.add() got conflicting "${firstEntry.field}" (${JSON.stringify(firstEntry.value)}) ` +
98
+ `and "${conflictEntry.field}" (${JSON.stringify(conflictEntry.value)}) id fields`);
63
99
  }
64
- return payload.id ?? payload._id ?? getDefaultId();
100
+ return firstEntry?.value ?? getDefaultId();
65
101
  }
66
102
  export function prepareAddPayload(value, idFields, docId) {
67
103
  const payload = value;
68
- if (idFields.includes('_id'))
69
- payload._id = docId;
70
- if (idFields.includes('id')) {
71
- payload.id = docId;
72
- }
73
- else if (payload.id === docId) {
74
- delete payload.id;
104
+ for (const field of idFields)
105
+ payload[field] = docId;
106
+ for (const field of LEGACY_ADD_ID_FIELDS) {
107
+ if (idFields.includes(field))
108
+ continue;
109
+ if (payload[field] === docId)
110
+ delete payload[field];
75
111
  }
76
112
  return value;
77
113
  }
@@ -93,3 +129,55 @@ export function isIdFieldPath(segments, idFields) {
93
129
  const last = segments[2];
94
130
  return typeof last === 'string' && idFields.includes(last);
95
131
  }
132
+ function normalizeIdFieldsConfig(idFields) {
133
+ if (!Array.isArray(idFields)) {
134
+ throw Error('Teamplay idFields config must be an array of field names');
135
+ }
136
+ const normalized = [];
137
+ for (const field of idFields) {
138
+ if (typeof field !== 'string' || field.length === 0) {
139
+ throw Error('Teamplay idFields config must contain only non-empty string field names');
140
+ }
141
+ if (!normalized.includes(field))
142
+ normalized.push(field);
143
+ }
144
+ if (normalized.length === 0) {
145
+ throw Error('Teamplay idFields config must contain at least one field name');
146
+ }
147
+ return Object.freeze(normalized);
148
+ }
149
+ function getAddIdEntries(payload, idFields) {
150
+ const fields = uniqueFields([...LEGACY_ADD_ID_FIELDS, ...idFields]);
151
+ const entries = [];
152
+ for (const field of fields) {
153
+ const value = payload[field];
154
+ if (value == null)
155
+ continue;
156
+ entries.push({ field, value: value });
157
+ }
158
+ return entries;
159
+ }
160
+ function uniqueFields(fields) {
161
+ const result = [];
162
+ for (const field of fields) {
163
+ if (!result.includes(field))
164
+ result.push(field);
165
+ }
166
+ return result;
167
+ }
168
+ function getGlobalRuntimeConfig(create = true) {
169
+ const holder = getGlobalRuntimeConfigHolder();
170
+ let config = holder[TEAMPLAY_RUNTIME_CONFIG_SYMBOL];
171
+ if (config == null && create) {
172
+ config = {};
173
+ holder[TEAMPLAY_RUNTIME_CONFIG_SYMBOL] = config;
174
+ }
175
+ if (config != null && (!isPlainObject(config))) {
176
+ throw Error('Teamplay runtime config must be an object');
177
+ }
178
+ return config;
179
+ }
180
+ function getGlobalRuntimeConfigHolder() {
181
+ return globalThis;
182
+ }
183
+ const LEGACY_ADD_ID_FIELDS = ['id', '_id'];
@@ -4,4 +4,6 @@ export type { RootSignal, TeamplayCollections, TeamplayFeature, TeamplayFeatures
4
4
  export declare const BaseModel: import("./Signal.js").SignalConstructor;
5
5
  export default BaseModel;
6
6
  export { defineModels, default as initModels, getModels, resetModelsForTests } from './initModels.js';
7
+ export { default as reaction } from './reaction.js';
8
+ export type { ReactionHandle, ReactionOptions } from './reaction.js';
7
9
  export { defineSchema } from '@teamplay/schema';
package/dist/orm/index.js CHANGED
@@ -3,4 +3,5 @@ export { belongsTo, hasMany, hasOne } from "./associations.js";
3
3
  export const BaseModel = Signal;
4
4
  export default BaseModel;
5
5
  export { defineModels, default as initModels, getModels, resetModelsForTests } from "./initModels.js";
6
+ export { default as reaction } from "./reaction.js";
6
7
  export { defineSchema } from '@teamplay/schema';
@@ -36,7 +36,7 @@ function warnIfSchemaWasNotDefined(pattern, schema) {
36
36
  warnedUnwrappedSchemaPatterns.add(pattern);
37
37
  console.warn(`[teamplay] Schema for model "${pattern}" was loaded as a plain object. ` +
38
38
  'Wrap it with defineSchema(schema) to enable the conventional schema setup. ' +
39
- 'Plain schemas still work for backward compatibility.');
39
+ 'Plain schemas still work.');
40
40
  }
41
41
  function shouldWarnAboutUnwrappedSchemas() {
42
42
  const env = getProcessEnv();
@@ -1,22 +1,7 @@
1
- export function getPrivateDataRoot(rootId: any, create?: boolean): {
2
- [x: string]: unknown;
3
- [x: number]: unknown;
4
- };
5
- export function getPrivateDataRawRoot(rootId: any, create?: boolean): {
6
- [x: string]: unknown;
7
- [x: number]: unknown;
8
- };
9
- export function getPrivateData(rootId: any, logicalSegments: any, raw?: boolean): unknown;
10
- export function setPrivateData(rootId: any, logicalSegments: any, value: any): void;
11
- export function setReplacePrivateData(rootId: any, logicalSegments: any, value: any): void;
12
- export function delPrivateData(rootId: any, logicalSegments: any, options?: {}): void;
13
- export function arrayPushPrivateData(rootId: any, logicalSegments: any, value: any): number | undefined;
14
- export function arrayUnshiftPrivateData(rootId: any, logicalSegments: any, value: any): number | undefined;
15
- export function arrayInsertPrivateData(rootId: any, logicalSegments: any, index: any, values: any): number | undefined;
16
- export function arrayPopPrivateData(rootId: any, logicalSegments: any): any;
17
- export function arrayShiftPrivateData(rootId: any, logicalSegments: any): any;
18
- export function arrayRemovePrivateData(rootId: any, logicalSegments: any, index: any, howMany?: number): any[] | undefined;
19
- export function arrayMovePrivateData(rootId: any, logicalSegments: any, from: any, to: any, howMany?: number): any[] | undefined;
20
- export function stringInsertPrivateData(rootId: any, logicalSegments: any, index: any, text: any): any;
21
- export function stringRemovePrivateData(rootId: any, logicalSegments: any, index: any, howMany: any): any;
22
- export function getPrivateDataSnapshot(rootId: any): any;
1
+ import type { PathSegment } from './types/path.js'
2
+
3
+ export function getPrivateData (
4
+ rootId: string | undefined,
5
+ logicalSegments: readonly PathSegment[],
6
+ raw?: boolean
7
+ ): unknown
@@ -1,6 +1,11 @@
1
+ import isServer from "../utils/isServer.js";
1
2
  import { getRootContext } from "./rootContext.js";
2
- import { getPrivateDataSegments, isPrivateCollectionSegments } from "./rootScope.js";
3
+ import { getPrivateDataSegments, isGlobalRootId, isPrivateCollectionSegments } from "./rootScope.js";
3
4
  import { arrayInsert as _arrayInsert, arrayMove as _arrayMove, arrayPop as _arrayPop, arrayPush as _arrayPush, arrayRemove as _arrayRemove, arrayShift as _arrayShift, arrayUnshift as _arrayUnshift, del as _del, set as _set, setReplace as _setReplace, stringInsertLocal as _stringInsertLocal, stringRemoveLocal as _stringRemoveLocal } from './dataTree.js';
5
+ const warnedGlobalRootPrivateCollections = new Set();
6
+ export function __resetPrivateDataWarningsForTests() {
7
+ warnedGlobalRootPrivateCollections.clear();
8
+ }
4
9
  export function getPrivateDataRoot(rootId, create = false) {
5
10
  return getRootContext(rootId, create)?.getPrivateDataRoot();
6
11
  }
@@ -24,6 +29,7 @@ export function setPrivateData(rootId, logicalSegments, value) {
24
29
  const context = getRootContext(rootId, true);
25
30
  if (!context)
26
31
  return;
32
+ warnGlobalRootPrivateMutation(rootId, logicalSegments);
27
33
  const segments = getPrivateDataSegments(logicalSegments);
28
34
  _set(segments, value, context.getPrivateDataRoot(), getModelEventContext(rootId, logicalSegments));
29
35
  }
@@ -34,6 +40,7 @@ export function setReplacePrivateData(rootId, logicalSegments, value) {
34
40
  const context = getRootContext(rootId, true);
35
41
  if (!context)
36
42
  return;
43
+ warnGlobalRootPrivateMutation(rootId, logicalSegments);
37
44
  const segments = getPrivateDataSegments(logicalSegments);
38
45
  _setReplace(segments, value, context.getPrivateDataRoot(), getModelEventContext(rootId, logicalSegments));
39
46
  }
@@ -43,6 +50,7 @@ export function delPrivateData(rootId, logicalSegments, options = {}) {
43
50
  const context = getRootContext(rootId, false);
44
51
  if (!context)
45
52
  return;
53
+ warnGlobalRootPrivateMutation(rootId, logicalSegments);
46
54
  const segments = getPrivateDataSegments(logicalSegments);
47
55
  _del(segments, context.getPrivateDataRoot(), getModelEventContext(rootId, logicalSegments));
48
56
  pruneEmptyPrivateParents(context.getPrivateDataRoot(), context.getPrivateDataRawRoot(), segments, options);
@@ -108,8 +116,19 @@ function getRequiredPrivateContext(rootId, logicalSegments, methodName) {
108
116
  if (!isPrivateCollectionSegments(logicalSegments)) {
109
117
  throw Error(`${methodName} expects private collection segments`);
110
118
  }
119
+ warnGlobalRootPrivateMutation(rootId, logicalSegments);
111
120
  return getRootContext(rootId, true);
112
121
  }
122
+ function warnGlobalRootPrivateMutation(rootId, logicalSegments) {
123
+ if (!isServer || !isGlobalRootId(rootId) || !isPrivateCollectionSegments(logicalSegments))
124
+ return;
125
+ const collection = String(logicalSegments[0]);
126
+ if (warnedGlobalRootPrivateCollections.has(collection))
127
+ return;
128
+ warnedGlobalRootPrivateCollections.add(collection);
129
+ console.warn(`[teamplay] Writing to private collection "${collection}" on the global server root. ` +
130
+ 'Use a request-scoped root, getRootSignal({ rootId }), or req.model for per-request private state.');
131
+ }
113
132
  function getModelEventContext(rootId, logicalSegments) {
114
133
  return {
115
134
  rootId,
@@ -0,0 +1,13 @@
1
+ import type { PathSegment } from './types/path.js'
2
+
3
+ export function isDocReady (segments: readonly PathSegment[]): boolean
4
+
5
+ export function isQueryReady (
6
+ collection: string,
7
+ idsSegments: readonly PathSegment[],
8
+ docsSegments: readonly PathSegment[],
9
+ extraSegments: readonly PathSegment[],
10
+ aggregationSegments: readonly PathSegment[],
11
+ isAggregate: boolean,
12
+ hasExtraResult: boolean
13
+ ): boolean
@@ -1,12 +1,12 @@
1
- import { getRaw } from '../dataTree.js';
2
- import { getConnection } from "../connection.js";
3
- import { isMissingShareDoc } from '../missingDoc.js';
4
- import { QUERIES, HASH, PARAMS, COLLECTION_NAME, querySubscriptions } from '../Query.js';
5
- import { AGGREGATIONS, IS_AGGREGATION, aggregationSubscriptions } from '../Aggregation.js';
6
- import { getPrivateData, setPrivateData } from '../privateData.js';
7
- import { getRoot, ROOT_ID } from "../Root.js";
8
- import { isRootContextClosed } from "../rootContext.js";
9
- import { getScopedSignalHash, normalizeRootId } from "../rootScope.js";
1
+ import { getRaw } from './dataTree.js';
2
+ import { getConnection } from "./connection.js";
3
+ import { isMissingShareDoc } from './missingDoc.js';
4
+ import { QUERIES, HASH, PARAMS, COLLECTION_NAME, querySubscriptions } from './Query.js';
5
+ import { AGGREGATIONS, IS_AGGREGATION, aggregationSubscriptions } from './Aggregation.js';
6
+ import { getPrivateData, setPrivateData } from './privateData.js';
7
+ import { getRoot, ROOT_ID } from "./Root.js";
8
+ import { isRootContextClosed } from "./rootContext.js";
9
+ import { getScopedSignalHash, normalizeRootId } from "./rootScope.js";
10
10
  let imperativeQueryReadyTimeoutMs = 1000;
11
11
  export function isQueryReady(collection, idsSegments, docsSegments, extraSegments, aggregationSegments, isAggregate, hasExtraResult) {
12
12
  if (hasExtraResult) {
@@ -157,7 +157,7 @@ function createImperativeQueryReadinessError($query, timeoutMs) {
157
157
  }
158
158
  }
159
159
  return Error(`
160
- Compat query did not fully materialize within ${timeoutMs}ms.
160
+ Query did not fully materialize within ${timeoutMs}ms.
161
161
  Collection: ${collection}
162
162
  Params: ${JSON.stringify(params)}
163
163
  Hash: ${hash}
@@ -0,0 +1,11 @@
1
+ export interface ReactionOptions {
2
+ lazy?: boolean;
3
+ debugger?: Function;
4
+ scheduler?: (run: () => unknown) => unknown;
5
+ onError?: (error: unknown) => unknown;
6
+ }
7
+ export interface ReactionHandle {
8
+ dispose: () => void;
9
+ }
10
+ export declare function reaction(fn: () => unknown, options?: ReactionOptions): ReactionHandle;
11
+ export default reaction;
@@ -0,0 +1,47 @@
1
+ import { observe, unobserve } from '@nx-js/observer-util';
2
+ import { scheduleReaction } from './batchScheduler.js';
3
+ export function reaction(fn, options = {}) {
4
+ if (typeof fn !== 'function')
5
+ throw Error('reaction() expects a function');
6
+ let disposed = false;
7
+ let pendingReactionFn;
8
+ function runReaction(runner) {
9
+ if (disposed)
10
+ return;
11
+ try {
12
+ return runner();
13
+ }
14
+ catch (error) {
15
+ if (typeof options.onError === 'function')
16
+ return options.onError(error);
17
+ throw error;
18
+ }
19
+ }
20
+ const scheduledRun = () => {
21
+ if (!pendingReactionFn)
22
+ return;
23
+ return runReaction(pendingReactionFn);
24
+ };
25
+ const runner = observe(fn, {
26
+ lazy: true,
27
+ debugger: options.debugger,
28
+ scheduler: (reactionFn) => {
29
+ pendingReactionFn = reactionFn;
30
+ if (typeof options.scheduler === 'function')
31
+ return options.scheduler(scheduledRun);
32
+ scheduleReaction(scheduledRun);
33
+ }
34
+ });
35
+ if (!options.lazy)
36
+ runReaction(runner);
37
+ return {
38
+ dispose() {
39
+ if (disposed)
40
+ return;
41
+ disposed = true;
42
+ pendingReactionFn = undefined;
43
+ unobserve(runner);
44
+ }
45
+ };
46
+ }
47
+ export default reaction;
@@ -2,18 +2,9 @@ import type { PathSegment } from './types/path.js';
2
2
  type RootId = string | null | undefined;
3
3
  type DataTree = Record<string | number, unknown>;
4
4
  type RuntimeKind = 'query' | 'aggregation';
5
- type ModelEventStore = Map<string, unknown>;
6
- type ActiveRefEntry = {
7
- stop?: () => void;
8
- };
9
5
  interface RootContextOptions {
10
6
  fetchOnly?: boolean;
11
7
  }
12
- interface ModelListeners {
13
- change: ModelEventStore;
14
- all: ModelEventStore;
15
- [eventName: string]: ModelEventStore;
16
- }
17
8
  export interface DirectDocSubscriptionEntry {
18
9
  segments: PathSegment[];
19
10
  count: number;
@@ -24,15 +15,11 @@ export default class RootContext {
24
15
  fetchOnly: boolean;
25
16
  privateDataRaw: DataTree;
26
17
  privateData: DataTree;
27
- readonly refLinks: Map<string, unknown>;
28
- readonly activeRefs: Map<string, ActiveRefEntry>;
29
- readonly modelListeners: ModelListeners;
30
18
  readonly queryRuntimeHashes: Set<string>;
31
19
  readonly aggregationRuntimeHashes: Set<string>;
32
20
  readonly signalHashes: Set<string>;
33
21
  readonly directDocSubscriptions: Map<string, DirectDocSubscriptionEntry>;
34
22
  constructor(rootId: RootId, { fetchOnly }?: RootContextOptions);
35
- getModelEventStore(eventName: string, create?: boolean): ModelEventStore;
36
23
  getFetchOnly(): boolean;
37
24
  setFetchOnly(value: boolean): void;
38
25
  getPrivateDataRoot(): DataTree;
@@ -48,9 +35,6 @@ export default class RootContext {
48
35
  unregisterSignalHash(signalHash: string | null | undefined): void;
49
36
  registerDirectDocSubscription(hash: string | null | undefined, segments: readonly PathSegment[], token?: unknown): void;
50
37
  unregisterDirectDocSubscription(hash: string | null | undefined, token?: unknown): void;
51
- resetRefs(): void;
52
- resetActiveRefs(): void;
53
- resetModelListeners(): void;
54
38
  resetRuntimeHashes(): void;
55
39
  resetPrivateData(): void;
56
40
  resetSignalHashes(): void;
@@ -12,12 +12,6 @@ export default class RootContext {
12
12
  fetchOnly;
13
13
  privateDataRaw;
14
14
  privateData;
15
- refLinks = new Map();
16
- activeRefs = new Map();
17
- modelListeners = {
18
- change: new Map(),
19
- all: new Map()
20
- };
21
15
  queryRuntimeHashes = new Set();
22
16
  aggregationRuntimeHashes = new Set();
23
17
  signalHashes = new Set();
@@ -28,14 +22,6 @@ export default class RootContext {
28
22
  this.privateDataRaw = {};
29
23
  this.privateData = observable(this.privateDataRaw);
30
24
  }
31
- getModelEventStore(eventName, create = false) {
32
- let store = this.modelListeners[eventName];
33
- if (!store && create) {
34
- store = new Map();
35
- this.modelListeners[eventName] = store;
36
- }
37
- return store;
38
- }
39
25
  getFetchOnly() {
40
26
  return !!this.fetchOnly;
41
27
  }
@@ -124,17 +110,6 @@ export default class RootContext {
124
110
  if (entry.count === 0)
125
111
  this.directDocSubscriptions.delete(hash);
126
112
  }
127
- resetRefs() {
128
- this.refLinks.clear();
129
- }
130
- resetActiveRefs() {
131
- this.activeRefs.clear();
132
- }
133
- resetModelListeners() {
134
- for (const store of Object.values(this.modelListeners)) {
135
- store.clear();
136
- }
137
- }
138
113
  resetRuntimeHashes() {
139
114
  this.queryRuntimeHashes.clear();
140
115
  this.aggregationRuntimeHashes.clear();
@@ -151,9 +126,6 @@ export default class RootContext {
151
126
  }
152
127
  isRuntimeEmpty() {
153
128
  return (isPlainObjectEmpty(this.privateData) &&
154
- this.refLinks.size === 0 &&
155
- this.activeRefs.size === 0 &&
156
- Object.values(this.modelListeners).every(store => store.size === 0) &&
157
129
  this.queryRuntimeHashes.size === 0 &&
158
130
  this.aggregationRuntimeHashes.size === 0 &&
159
131
  this.signalHashes.size === 0 &&
@@ -36,14 +36,14 @@ export function getSignalId($signal, rootId, readPath) {
36
36
  throw Error('Can\'t get the id of a collection');
37
37
  if (isDirectPublicDocumentSegments(segments))
38
38
  return getLeafId(segments);
39
+ if (segments[0] === AGGREGATIONS && segments.length === 3) {
40
+ return getAggregationDocId(segments, rootId, readPath);
41
+ }
39
42
  if (readPath) {
40
43
  const valueId = getValueIdFromPaths(segments, readPath);
41
44
  if (valueId.found)
42
45
  return valueId.id;
43
46
  }
44
- if (segments[0] === AGGREGATIONS && segments.length === 3) {
45
- return getAggregationDocId(segments, rootId);
46
- }
47
47
  return getLeafId(segments);
48
48
  }
49
49
  export function getSignalCollection($signal) {
@@ -1,4 +1,4 @@
1
- import { AGGREGATIONS, IS_AGGREGATION } from './Aggregation.js';
1
+ import { AGGREGATIONS, IS_AGGREGATION, getAggregationCollectionName, getAggregationRowId } from './Aggregation.js';
2
2
  import { HASH, IS_QUERY, QUERIES } from './Query.js';
3
3
  import { SEGMENTS } from "./signalSymbols.js";
4
4
  export function readSignalValue($signal, context, method, rawMethod) {
@@ -41,7 +41,8 @@ export function getSignalIds($signal, context) {
41
41
  const docs = context.readPrivateData(rootId, $signal[SEGMENTS], false);
42
42
  if (!Array.isArray(docs))
43
43
  return [];
44
- return docs.map(getAggregationRowId).filter(isString);
44
+ const collectionName = getAggregationCollectionName($signal[SEGMENTS]);
45
+ return docs.map(doc => getAggregationRowId(doc, collectionName)).filter(isString);
45
46
  }
46
47
  context.error('Signal.getIds() can only be used on query signals or aggregation signals. ' +
47
48
  'Received a regular signal: ' + JSON.stringify($signal[SEGMENTS]));
@@ -55,13 +56,6 @@ export function isAggregationValueSignal($signal) {
55
56
  const segments = $signal[SEGMENTS];
56
57
  return segments.length >= 2 && segments[0] === AGGREGATIONS;
57
58
  }
58
- function getAggregationRowId(doc) {
59
- const row = doc;
60
- if (typeof row?._id === 'string')
61
- return row._id;
62
- if (typeof row?.id === 'string')
63
- return row.id;
64
- }
65
59
  function isString(value) {
66
60
  return typeof value === 'string';
67
61
  }
@@ -3,7 +3,6 @@ type MaybePromise<TValue> = TValue | Promise<TValue>;
3
3
  export interface SignalStorageMutationContext<TSignal> {
4
4
  getOwningRootId: ($signal: TSignal) => string | undefined;
5
5
  isPublicCollection: (segment: PathSegment | undefined) => boolean;
6
- isPrivateMutationForbidden: () => boolean;
7
6
  }
8
7
  export interface SignalStorageMutationHandlers<TResult> {
9
8
  public: (segments: PathSegment[]) => MaybePromise<TResult>;
@@ -14,5 +13,4 @@ export interface SignalStorageMutationResult<TResult> {
14
13
  value: TResult | undefined;
15
14
  }
16
15
  export declare function runSignalStorageMutation<TSignal, TResult>($signal: TSignal, context: SignalStorageMutationContext<TSignal>, segments: PathSegment[], handlers: SignalStorageMutationHandlers<TResult>): Promise<SignalStorageMutationResult<TResult>>;
17
- export declare function ensurePrivateMutationAllowed<TSignal>(context: Pick<SignalStorageMutationContext<TSignal>, 'isPrivateMutationForbidden'>): void;
18
16
  export {};
@@ -10,17 +10,8 @@ export async function runSignalStorageMutation($signal, context, segments, handl
10
10
  value: await handlers.public(segments)
11
11
  };
12
12
  }
13
- ensurePrivateMutationAllowed(context);
14
13
  return {
15
14
  skipped: false,
16
15
  value: await handlers.private(context.getOwningRootId($signal), segments)
17
16
  };
18
17
  }
19
- export function ensurePrivateMutationAllowed(context) {
20
- if (!context.isPrivateMutationForbidden())
21
- return;
22
- throw Error(`
23
- Can't modify private collections data when 'publicOnly' is enabled.
24
- On the server you can only work with public collections.
25
- `);
26
- }
@@ -2,4 +2,4 @@ export const SEGMENTS = Symbol('path segments targeting the particular node in t
2
2
  export const ARRAY_METHOD = Symbol('run array method on the signal');
3
3
  export const GET = Symbol('get the value of the signal - either observed or raw');
4
4
  export const GETTERS = Symbol('get the list of this signal\'s getters');
5
- export const DEFAULT_GETTERS = ['path', 'root', 'id', 'get', 'peek', 'getId', 'map', 'reduce', 'find', 'getIds', 'getExtra', 'getCollection'];
5
+ export const DEFAULT_GETTERS = ['path', 'root', 'id', 'get', 'peek', 'getId', 'map', 'reduce', 'find', 'getIds', 'getExtra', 'getCopy', 'getDeepCopy', 'getCollection'];
@@ -1,4 +1,4 @@
1
- import { type SignalStorageMutationContext } from './signalStorageMutations.js';
1
+ import type { SignalStorageMutationContext } from './signalStorageMutations.js';
2
2
  import { SEGMENTS } from './signalSymbols.js';
3
3
  import type { PathSegment } from './types/path.js';
4
4
  export interface SignalValueMutationOwner {
@@ -1,5 +1,4 @@
1
1
  import { getIdFieldsForSegments, isIdFieldPath, isPublicDocPath, normalizeIdFields } from "./idFields.js";
2
- import { ensurePrivateMutationAllowed } from "./signalStorageMutations.js";
3
2
  import { SEGMENTS } from "./signalSymbols.js";
4
3
  export async function setSignalValue($signal, context, value) {
5
4
  const segments = $signal[SEGMENTS];
@@ -15,7 +14,6 @@ export async function setSignalValue($signal, context, value) {
15
14
  await context.setPublicDoc(segments, nextValue);
16
15
  return;
17
16
  }
18
- ensurePrivateMutationAllowed(context);
19
17
  context.setPrivateData(context.getOwningRootId($signal), segments, nextValue);
20
18
  }
21
19
  export async function deleteSignalValue($signal, context) {
@@ -31,6 +29,5 @@ export async function deleteSignalValue($signal, context) {
31
29
  await context.deletePublicDoc(segments);
32
30
  return;
33
31
  }
34
- ensurePrivateMutationAllowed(context);
35
32
  context.deletePrivateData(context.getOwningRootId($signal), segments);
36
33
  }
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;