teamplay 0.5.0-alpha.30 → 0.5.0-alpha.32

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.
@@ -0,0 +1,2 @@
1
+ export { TEAMPLAY_RUNTIME_CONFIG_SYMBOL, configureTeamplay, getTeamplayConfig, getDefaultIdFields, setDefaultIdFields } from './orm/idFields.js';
2
+ export type { IdField, IdFields, TeamplayRuntimeConfig } from './orm/idFields.js';
package/dist/config.js ADDED
@@ -0,0 +1 @@
1
+ export { TEAMPLAY_RUNTIME_CONFIG_SYMBOL, configureTeamplay, getTeamplayConfig, getDefaultIdFields, setDefaultIdFields } from "./orm/idFields.js";
@@ -1 +1 @@
1
- export default function connect(options: any): void;
1
+ export default function connect(options?: {}): void;
@@ -1,9 +1,13 @@
1
1
  import Socket from '@teamplay/channel';
2
2
  import Connection from './sharedbConnection.cjs';
3
3
  import { connection, setConnection } from "../orm/connection.js";
4
- export default function connect(options) {
4
+ import { configureTeamplay } from "../config.js";
5
+ export default function connect(options = {}) {
6
+ const { idFields, ...socketOptions } = options || {};
7
+ if (idFields !== undefined)
8
+ configureTeamplay({ idFields });
5
9
  if (connection)
6
10
  return;
7
- const socket = new Socket(options);
11
+ const socket = new Socket(socketOptions);
8
12
  setConnection(new Connection(socket));
9
13
  }
@@ -1 +1 @@
1
- export default function createConnectWithPersistence({ storage, createPubsub }?: {}): () => Promise<void>;
1
+ export default function createConnectWithPersistence({ storage, createPubsub }?: {}): (options?: {}) => Promise<void>;
@@ -3,21 +3,25 @@
3
3
  import ShareDbMingo from '@startupjs/sharedb-mingo-memory';
4
4
  import ShareBackend from 'sharedb';
5
5
  import { connection, setConnection } from "../../orm/connection.js";
6
+ import { configureTeamplay } from "../../config.js";
6
7
  const STORAGE_NAMESPACE = 'teamplay-offline';
7
8
  const DOCS_PREFIX = `${STORAGE_NAMESPACE}:docs:`;
8
9
  const LAST_OP_PREFIX = `${STORAGE_NAMESPACE}:last-op:`;
9
10
  export default function createConnectWithPersistence({ storage, createPubsub } = {}) {
10
11
  if (!storage)
11
12
  throw new Error('[connect-offline] storage is required');
12
- return async function connect() {
13
+ return async function connect(options = {}) {
14
+ const { idFields } = options || {};
15
+ if (idFields !== undefined)
16
+ configureTeamplay({ idFields });
13
17
  if (connection)
14
18
  return;
15
19
  const db = new ShareDbMingo();
16
- const options = { db };
20
+ const backendOptions = { db };
17
21
  const { pubsub } = (await init(db, storage, createPubsub)) || {};
18
22
  if (pubsub)
19
- options.pubsub = pubsub;
20
- const backend = new ShareBackend(options);
23
+ backendOptions.pubsub = pubsub;
24
+ const backend = new ShareBackend(backendOptions);
21
25
  setConnection(backend.connect());
22
26
  };
23
27
  }
@@ -3,7 +3,7 @@ export namespace storage {
3
3
  export { setItem };
4
4
  export { iterate };
5
5
  }
6
- declare const _default: () => Promise<void>;
6
+ declare const _default: (options?: {}) => Promise<void>;
7
7
  export default _default;
8
8
  declare function getItem(key: any): Promise<any>;
9
9
  declare function setItem(key: any, value: any): Promise<any>;
@@ -4,6 +4,6 @@ export namespace storage {
4
4
  function setItem(key: any, value: any): Promise<any>;
5
5
  function iterate(iterator: any): Promise<any>;
6
6
  }
7
- declare const _default: () => Promise<void>;
7
+ declare const _default: (options?: {}) => Promise<void>;
8
8
  export default _default;
9
9
  import SharedbCrosstabPubsub from '../lib/sharedb-crosstab-pubsub.cjs';
@@ -1 +1 @@
1
- export default function connect(): void;
1
+ export default function connect(options?: {}): void;
@@ -4,7 +4,11 @@
4
4
  import ShareDbMingo from '@startupjs/sharedb-mingo-memory';
5
5
  import ShareBackend from 'sharedb';
6
6
  import { connection, setConnection } from "../orm/connection.js";
7
- export default function connect() {
7
+ import { configureTeamplay } from "../config.js";
8
+ export default function connect(options = {}) {
9
+ const { idFields } = options || {};
10
+ if (idFields !== undefined)
11
+ configureTeamplay({ idFields });
8
12
  if (connection)
9
13
  return;
10
14
  const backend = new ShareBackend({ db: new ShareDbMingo() });
package/dist/index.d.ts CHANGED
@@ -59,9 +59,13 @@ export { default as useSub, useAsyncSub, useBatchSub, setUseDeferredValue as __s
59
59
  export { default as useSuspendMemo, useSuspendMemoByKey } from './react/useSuspendMemo.js';
60
60
  export declare const observer: ObserverFunction;
61
61
  export { emit, useOn, useEmit } from './orm/Compat/eventsCompat.js';
62
+ export { default as reaction } from './orm/reaction.js';
63
+ export type { ReactionHandle, ReactionOptions } from './orm/reaction.js';
62
64
  export { useDidUpdate, useOnce, useSyncEffect } from './react/helpers.js';
63
65
  export { connection, setConnection, getConnection, getDefaultFetchOnly, setDefaultFetchOnly, publicOnly, setPublicOnly } from './orm/connection.js';
64
66
  export type { TeamplayConnection, TeamplayShareDoc } from './orm/connection.js';
67
+ export { TEAMPLAY_RUNTIME_CONFIG_SYMBOL, configureTeamplay, getTeamplayConfig, getDefaultIdFields, setDefaultIdFields } from './config.js';
68
+ export type { IdField, IdFields, TeamplayRuntimeConfig } from './config.js';
65
69
  export { getSubscriptionGcDelay, setSubscriptionGcDelay } from './orm/subscriptionGcDelay.js';
66
70
  export { useId, useNow, useScheduleUpdate, useTriggerUpdate } from './react/helpers.js';
67
71
  export { GUID_PATTERN, defineSchema, hasMany, hasOne, hasManyFlags, belongsTo, pickFormFields } from '@teamplay/schema';
package/dist/index.js CHANGED
@@ -19,8 +19,10 @@ export { default as useSub, useAsyncSub, useBatchSub, setUseDeferredValue as __s
19
19
  export { default as useSuspendMemo, useSuspendMemoByKey } from "./react/useSuspendMemo.js";
20
20
  export const observer = runtimeObserver;
21
21
  export { emit, useOn, useEmit } from './orm/Compat/eventsCompat.js';
22
+ export { default as reaction } from "./orm/reaction.js";
22
23
  export { useDidUpdate, useOnce, useSyncEffect } from "./react/helpers.js";
23
24
  export { connection, setConnection, getConnection, getDefaultFetchOnly, setDefaultFetchOnly, publicOnly, setPublicOnly } from "./orm/connection.js";
25
+ export { TEAMPLAY_RUNTIME_CONFIG_SYMBOL, configureTeamplay, getTeamplayConfig, getDefaultIdFields, setDefaultIdFields } from "./config.js";
24
26
  export { getSubscriptionGcDelay, setSubscriptionGcDelay } from "./orm/subscriptionGcDelay.js";
25
27
  export { useId, useNow, useScheduleUpdate, useTriggerUpdate } from "./react/helpers.js";
26
28
  export { GUID_PATTERN, defineSchema, hasMany, hasOne, hasManyFlags, belongsTo, pickFormFields } from '@teamplay/schema';
@@ -9,16 +9,15 @@ import { getIdFieldsForSegments, isIdFieldPath, isPublicDocPath, normalizeIdFiel
9
9
  import { incrementPublic as _incrementPublic, arrayPushPublic as _arrayPushPublic, arrayUnshiftPublic as _arrayUnshiftPublic, arrayInsertPublic as _arrayInsertPublic, arrayPopPublic as _arrayPopPublic, arrayShiftPublic as _arrayShiftPublic, arrayRemovePublic as _arrayRemovePublic, arrayMovePublic as _arrayMovePublic, setPublicDocReplace as _setPublicDocReplace, stringInsertPublic as _stringInsertPublic, stringRemovePublic as _stringRemovePublic } from '../dataTree.js';
10
10
  import { on as onCustomEvent, removeListener as removeCustomEventListener } from './eventsCompat.js';
11
11
  import { waitForImperativeQueryReady } from './queryReadiness.js';
12
- import { isModelEventsEnabled, normalizePattern, onModelEvent, removeModelListener } from './modelEvents.js';
12
+ import { isModelEventsEnabled } from './modelEvents.js';
13
13
  import { setRefLink, removeRefLink, getAllRefLinks } from './refRegistry.js';
14
14
  import { REF_TARGET, resolveRefSignalSafe, resolveRefSegmentsSafe } from './refFallback.js';
15
15
  import { runInBatch } from '../batchScheduler.js';
16
- import { runInSilentContext, runInModelEventsSilentContext, isSilentContextActive } from './silentContext.js';
16
+ import { runInModelEventsSilentContext, isSilentContextActive } from './silentContext.js';
17
17
  import universal$ from "../../react/universal$.js";
18
18
  import { getRootContext } from "../rootContext.js";
19
19
  import { arrayInsertPrivateData, arrayMovePrivateData, arrayPopPrivateData, arrayPushPrivateData, arrayRemovePrivateData, arrayShiftPrivateData, arrayUnshiftPrivateData, delPrivateData, setReplacePrivateData, stringInsertPrivateData, stringRemovePrivateData } from '../privateData.js';
20
20
  class SignalCompat extends Signal {
21
- static ID_FIELDS = ['_id'];
22
21
  static [GETTERS] = DEFAULT_GETTERS;
23
22
  path() {
24
23
  if (arguments.length > 0)
@@ -68,12 +67,6 @@ class SignalCompat extends Signal {
68
67
  return this.extra.get();
69
68
  return undefined;
70
69
  }
71
- silent(value) {
72
- if (arguments.length > 1)
73
- throw Error('Signal.silent() expects zero or one argument');
74
- const enabled = value == null ? true : !!value;
75
- return createSilentSignalWrapper(this, enabled);
76
- }
77
70
  get() {
78
71
  if (arguments.length > 0)
79
72
  throw Error('Signal.get() does not accept any arguments');
@@ -299,15 +292,8 @@ class SignalCompat extends Signal {
299
292
  on(eventName, pattern, handler) {
300
293
  if (arguments.length < 2)
301
294
  throw Error('Signal.on() expects at least two arguments');
302
- if (eventName === 'change' || eventName === 'all') {
303
- if (typeof pattern === 'function') {
304
- return onCustomEvent(eventName, pattern);
305
- }
306
- if (typeof handler !== 'function')
307
- throw Error('Signal.on() expects a handler function');
308
- const normalized = normalizePattern(pattern, 'Signal.on()');
309
- const rootId = (getRoot(this) || this)?.[ROOT_ID];
310
- return onModelEvent(rootId, eventName, normalized, handler);
295
+ if ((eventName === 'change' || eventName === 'all') && typeof pattern !== 'function') {
296
+ throw Error('Signal model events are not supported. Use reaction() for signal changes.');
311
297
  }
312
298
  if (typeof pattern !== 'function')
313
299
  throw Error('Signal.on() expects a handler function');
@@ -316,16 +302,8 @@ class SignalCompat extends Signal {
316
302
  once(eventName, pattern, handler) {
317
303
  if (arguments.length < 2)
318
304
  throw Error('Signal.once() expects at least two arguments');
319
- const isModelEvent = eventName === 'change' || eventName === 'all';
320
- if (isModelEvent && typeof pattern !== 'function') {
321
- if (typeof handler !== 'function')
322
- throw Error('Signal.once() expects a handler function');
323
- const onceHandler = (...args) => {
324
- this.removeListener(eventName, onceHandler);
325
- handler(...args);
326
- };
327
- this.on(eventName, pattern, onceHandler);
328
- return onceHandler;
305
+ if ((eventName === 'change' || eventName === 'all') && typeof pattern !== 'function') {
306
+ throw Error('Signal model events are not supported. Use reaction() for signal changes.');
329
307
  }
330
308
  if (typeof pattern !== 'function')
331
309
  throw Error('Signal.once() expects a handler function');
@@ -339,10 +317,6 @@ class SignalCompat extends Signal {
339
317
  removeListener(eventName, handler) {
340
318
  if (arguments.length !== 2)
341
319
  throw Error('Signal.removeListener() expects two arguments');
342
- if (eventName === 'change' || eventName === 'all') {
343
- const rootId = (getRoot(this) || this)?.[ROOT_ID];
344
- return removeModelListener(rootId, eventName, handler);
345
- }
346
320
  return removeCustomEventListener(eventName, handler);
347
321
  }
348
322
  ref(path, target, options) {
@@ -434,53 +408,6 @@ class SignalCompat extends Signal {
434
408
  delete this[REF_TARGET];
435
409
  }
436
410
  }
437
- const SILENT_WRAPPER = Symbol('compat silent wrapper');
438
- const SILENT_WRAPPER_TARGET = Symbol('compat silent wrapper target');
439
- const SILENT_WRAPPER_ENABLED = Symbol('compat silent wrapper enabled');
440
- function createSilentSignalWrapper($signal, enabled = true) {
441
- if (!$signal || typeof $signal !== 'function')
442
- return $signal;
443
- if ($signal[SILENT_WRAPPER]) {
444
- const target = $signal[SILENT_WRAPPER_TARGET] || $signal;
445
- return createSilentSignalWrapper(target, enabled);
446
- }
447
- const handler = {
448
- get(target, key, receiver) {
449
- if (key === SILENT_WRAPPER)
450
- return true;
451
- if (key === SILENT_WRAPPER_TARGET)
452
- return target;
453
- if (key === SILENT_WRAPPER_ENABLED)
454
- return enabled;
455
- if (key === 'silent') {
456
- return function silentWrapper(value) {
457
- if (arguments.length > 1)
458
- throw Error('Signal.silent() expects zero or one argument');
459
- const nextEnabled = value == null ? true : !!value;
460
- return createSilentSignalWrapper(target, nextEnabled);
461
- };
462
- }
463
- const value = Reflect.get(target, key, receiver);
464
- if (isSignalLike(value)) {
465
- return createSilentSignalWrapper(value, enabled);
466
- }
467
- if (typeof value === 'function') {
468
- return function wrappedMethod(...args) {
469
- if (!enabled)
470
- return Reflect.apply(value, target, args);
471
- return runInSilentContext(() => Reflect.apply(value, target, args));
472
- };
473
- }
474
- return value;
475
- },
476
- apply(target, thisArg, args) {
477
- if (!enabled)
478
- return Reflect.apply(target, thisArg, args);
479
- return runInSilentContext(() => Reflect.apply(target, thisArg, args));
480
- }
481
- };
482
- return new Proxy($signal, handler);
483
- }
484
411
  function getRefStore($signal) {
485
412
  const $root = getRoot($signal) || $signal;
486
413
  const rootId = $root?.[ROOT_ID];
@@ -1,5 +1,5 @@
1
1
  import { useLayoutEffect } from 'react';
2
- import { isModelEventsEnabled, normalizePattern, onModelEvent, removeModelListener, __resetModelEventsForTests } from './modelEvents.js';
2
+ import { __resetModelEventsForTests } from './modelEvents.js';
3
3
  const listeners = new Map();
4
4
  export function emit(eventName, ...args) {
5
5
  const subs = listeners.get(eventName);
@@ -27,34 +27,19 @@ export function removeListener(eventName, handler) {
27
27
  }
28
28
  export function useOn(eventName, patternOrHandler, handler, deps) {
29
29
  const isModelEvent = eventName === 'change' || eventName === 'all';
30
- const isCustom = !isModelEvent || typeof patternOrHandler === 'function';
31
- if (isCustom) {
30
+ if (!isModelEvent || typeof patternOrHandler === 'function') {
32
31
  if (typeof patternOrHandler !== 'function')
33
32
  throw Error('useOn() expects a handler function');
34
33
  }
35
34
  else {
36
- if (typeof handler !== 'function')
37
- throw Error('useOn() expects a handler function');
35
+ throw Error('Signal model events are not supported. Use reaction() for signal changes.');
38
36
  }
39
- const normalizedPattern = isCustom ? null : normalizePatternMaybe(patternOrHandler);
40
37
  useLayoutEffect(() => {
41
- if (isCustom) {
42
- const listener = on(eventName, patternOrHandler);
43
- return () => {
44
- removeListener(eventName, listener);
45
- };
46
- }
47
- if (normalizedPattern == null) {
48
- handler(patternOrHandler);
49
- return;
50
- }
51
- if (!isModelEventsEnabled())
52
- return;
53
- const listener = onModelEvent(undefined, eventName, normalizedPattern, handler);
38
+ const listener = on(eventName, patternOrHandler);
54
39
  return () => {
55
- removeModelListener(undefined, eventName, listener);
40
+ removeListener(eventName, listener);
56
41
  };
57
- }, [eventName, patternOrHandler, handler, deps, normalizedPattern, isCustom]);
42
+ }, [eventName, patternOrHandler, deps]);
58
43
  }
59
44
  export function useEmit() {
60
45
  return emit;
@@ -63,11 +48,3 @@ export function __resetEventsForTests() {
63
48
  listeners.clear();
64
49
  __resetModelEventsForTests();
65
50
  }
66
- function normalizePatternMaybe(pattern) {
67
- try {
68
- return normalizePattern(pattern);
69
- }
70
- catch {
71
- return null;
72
- }
73
- }
@@ -1,5 +1,4 @@
1
1
  export function isSilentContextActive(): boolean;
2
2
  export function isModelEventsSilentContextActive(): boolean;
3
- export function runInSilentContext(fn: any): any;
4
3
  export function runInModelEventsSilentContext(fn: any): any;
5
4
  export function __resetSilentContextForTests(): void;
@@ -1,20 +1,10 @@
1
- let silentDepth = 0;
2
1
  let modelEventsSilentDepth = 0;
3
2
  export function isSilentContextActive() {
4
- return silentDepth > 0;
3
+ return false;
5
4
  }
6
5
  export function isModelEventsSilentContextActive() {
7
6
  return modelEventsSilentDepth > 0;
8
7
  }
9
- export function runInSilentContext(fn) {
10
- silentDepth += 1;
11
- try {
12
- return fn();
13
- }
14
- finally {
15
- silentDepth -= 1;
16
- }
17
- }
18
8
  export function runInModelEventsSilentContext(fn) {
19
9
  modelEventsSilentDepth += 1;
20
10
  try {
@@ -25,6 +15,5 @@ export function runInModelEventsSilentContext(fn) {
25
15
  }
26
16
  }
27
17
  export function __resetSilentContextForTests() {
28
- silentDepth = 0;
29
18
  modelEventsSilentDepth = 0;
30
19
  }
@@ -2,7 +2,7 @@ import { ARRAY_METHOD, DEFAULT_GETTERS, GET, GETTERS, SEGMENTS } from './signalS
2
2
  export { SEGMENTS, ARRAY_METHOD, GET, GETTERS, DEFAULT_GETTERS };
3
3
  export declare class Signal<TValue = unknown> extends Function {
4
4
  /** Fields that are treated as document ids and mirror the document id segment. */
5
- static ID_FIELDS: readonly ["_id"];
5
+ static get ID_FIELDS(): import("./idFields.js").IdFields;
6
6
  /** Method names that keep method binding priority over child signal dot access. */
7
7
  static [GETTERS]: string[];
8
8
  /** Association metadata registered for this model class. */
@@ -21,10 +21,9 @@ import { docSubscriptions } from './Doc.js';
21
21
  import { IS_QUERY, HASH, QUERIES } from './Query.js';
22
22
  import { AGGREGATIONS, IS_AGGREGATION, getAggregationCollectionName, getAggregationDocId } from './Aggregation.js';
23
23
  import { ROOT_FUNCTION, ROOT_ID, closeRootSignal, getRoot } from "./Root.js";
24
- import { DEFAULT_ID_FIELDS, getIdFieldsForSegments, isIdFieldPath, isPlainObject, isPublicDocPath, normalizeIdFields, prepareAddPayload, resolveAddDocId } from "./idFields.js";
24
+ import { getDefaultIdFields, getIdFieldsForSegments, isIdFieldPath, isPlainObject, isPublicDocPath, normalizeIdFields, prepareAddPayload, resolveAddDocId } from "./idFields.js";
25
25
  import { isCompatEnv } from './compatEnv.js';
26
26
  import { resolveRefSegmentsSafe, resolveRefSignalSafe } from './Compat/refFallback.js';
27
- import { compatStartOnRoot, compatStopOnRoot, joinScopePath } from './Compat/startStopCompat.js';
28
27
  import { runInBatch } from './batchScheduler.js';
29
28
  import { isPublicCollection } from "./signalPathKind.js";
30
29
  import { ARRAY_METHOD, DEFAULT_GETTERS, GET, GETTERS, SEGMENTS } from "./signalSymbols.js";
@@ -91,7 +90,9 @@ const SIGNAL_VALUE_MUTATION_CONTEXT = {
91
90
  };
92
91
  export class Signal extends Function {
93
92
  /** Fields that are treated as document ids and mirror the document id segment. */
94
- static ID_FIELDS = DEFAULT_ID_FIELDS;
93
+ static get ID_FIELDS() {
94
+ return getDefaultIdFields();
95
+ }
95
96
  /** Method names that keep method binding priority over child signal dot access. */
96
97
  static [GETTERS] = DEFAULT_GETTERS;
97
98
  /** Association metadata registered for this model class. */
@@ -578,8 +579,10 @@ export class Signal extends Function {
578
579
  async add(value) {
579
580
  if (arguments.length > 1)
580
581
  throw Error('Signal.add() expects a single argument');
581
- const id = resolveAddDocId(value, uuid);
582
- const idFields = getIdFieldsForSegments([this[SEGMENTS][0], id]);
582
+ const collection = this[SEGMENTS][0];
583
+ const collectionIdFields = getIdFieldsForSegments([collection, '']);
584
+ const id = resolveAddDocId(value, collectionIdFields, uuid);
585
+ const idFields = getIdFieldsForSegments([collection, id]);
583
586
  await this[id].set(prepareAddPayload(value, idFields, id));
584
587
  return id;
585
588
  }
@@ -849,23 +852,6 @@ export const extremelyLateBindings = {
849
852
  }
850
853
  }
851
854
  }
852
- if (key === 'start') {
853
- const [relativePath, ...depsAndGetter] = argumentsList;
854
- if (typeof relativePath !== 'string')
855
- throw Error('Signal.start() expects targetPath to be a string');
856
- const absolutePath = joinScopePath($parent.path(), relativePath);
857
- return compatStartOnRoot(getRoot($parent) || $parent, absolutePath, ...depsAndGetter);
858
- }
859
- if (key === 'stop') {
860
- if (argumentsList.length > 1)
861
- throw Error('Signal.stop() expects zero or one argument');
862
- const relativePath = argumentsList.length === 0 ? '' : argumentsList[0];
863
- if (relativePath != null && typeof relativePath !== 'string') {
864
- throw Error('Signal.stop() expects targetPath to be a string');
865
- }
866
- const absolutePath = joinScopePath($parent.path(), relativePath || '');
867
- return compatStopOnRoot(getRoot($parent) || $parent, absolutePath);
868
- }
869
855
  }
870
856
  throw Error(ERRORS.noSignalKey($parent, key));
871
857
  },
@@ -1,7 +1,7 @@
1
- export function beginBatch(): void;
2
- export function endBatch(): void;
3
- export function inBatch(): boolean;
4
- export function runInBatch(fn: any): any;
5
- export function scheduleReaction(reactionFn: any): void;
6
- export function flushReactions(): void;
7
- export function __resetBatchSchedulerForTests(): void;
1
+ export function beginBatch (): void
2
+ export function endBatch (): void
3
+ export function inBatch (): boolean
4
+ export function runInBatch<TResult> (fn: () => TResult): TResult
5
+ export function scheduleReaction (reactionFn: () => unknown): void
6
+ export function flushReactions (): void
7
+ export function __resetBatchSchedulerForTests (): void
@@ -2,13 +2,22 @@ import type { PathSegment } from './types/path.js';
2
2
  export type IdField = string;
3
3
  export type IdFields = readonly IdField[];
4
4
  export type PlainObject = Record<string, unknown>;
5
+ export interface TeamplayRuntimeConfig {
6
+ idFields?: IdFields | null;
7
+ }
5
8
  export declare const DEFAULT_ID_FIELDS: readonly ["_id"];
9
+ export declare const TEAMPLAY_RUNTIME_CONFIG_SYMBOL: unique symbol;
6
10
  export declare function getIdFieldsForSegments(segments: PathSegment[]): IdFields;
11
+ export declare function configureTeamplay({ idFields }?: TeamplayRuntimeConfig): void;
12
+ export declare function getTeamplayConfig(): Required<TeamplayRuntimeConfig>;
13
+ export declare function getDefaultIdFields(): IdFields;
14
+ export declare function setDefaultIdFields(idFields?: IdFields): void;
15
+ export declare function __resetTeamplayConfigForTests(): void;
7
16
  export declare function isPlainObject(value: unknown): value is PlainObject;
8
17
  export declare function injectIdFields<TValue>(value: TValue, idFields: IdFields, docId: PathSegment): TValue;
9
18
  export declare function normalizeIdFields<TValue>(value: TValue, idFields: IdFields, docId: PathSegment): TValue;
10
19
  export declare function stripIdFields<TValue>(value: TValue, idFields: IdFields): TValue;
11
- export declare function resolveAddDocId(value: unknown, getDefaultId: () => string): PathSegment;
20
+ export declare function resolveAddDocId(value: unknown, idFields: IdFields, getDefaultId: () => string): PathSegment;
12
21
  export declare function prepareAddPayload<TValue extends object>(value: TValue, idFields: IdFields, docId: PathSegment): TValue;
13
22
  export declare function isPublicDocPath(segments: unknown): segments is [string, PathSegment];
14
23
  export declare function isIdFieldPath(segments: unknown, idFields: IdFields): segments is [string, PathSegment, string];
@@ -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';
@@ -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;
package/dist/server.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export function createBackend(options?: {}): any;
2
- export function initConnection(backend: any, { fetchOnly, publicOnly, ...options }?: {
2
+ export function initConnection(backend: any, { fetchOnly, publicOnly, idFields, ...options }?: {
3
3
  fetchOnly?: boolean | undefined;
4
4
  publicOnly?: boolean | undefined;
5
5
  }): {
package/dist/server.js CHANGED
@@ -2,6 +2,7 @@ import createChannel from '@teamplay/channel/server';
2
2
  import backendCreateBackend from '@teamplay/backend';
3
3
  import { getModels } from "./orm/initModels.js";
4
4
  import { connection, setConnection, setDefaultFetchOnly, setPublicOnly } from "./orm/connection.js";
5
+ import { configureTeamplay } from "./config.js";
5
6
  export { default as ShareDB } from 'sharedb';
6
7
  export { mongo, mongoClient, createMongoIndex, redis, redlock, sqlite, getRedis, Redis, getRedisOptions, redisPrefix, generateRedisPrefix } from '@teamplay/backend';
7
8
  export function createBackend(options = {}) {
@@ -16,11 +17,13 @@ export function createBackend(options = {}) {
16
17
  return backendCreateBackend(nextOptions);
17
18
  }
18
19
  export default createBackend;
19
- export function initConnection(backend, { fetchOnly = true, publicOnly = true, ...options } = {}) {
20
+ export function initConnection(backend, { fetchOnly = true, publicOnly = true, idFields, ...options } = {}) {
20
21
  if (!backend)
21
22
  throw Error('backend is required');
22
23
  if (connection)
23
24
  throw Error('Connection already exists');
25
+ if (idFields !== undefined)
26
+ configureTeamplay({ idFields });
24
27
  setConnection(backend.connect());
25
28
  setDefaultFetchOnly(fetchOnly);
26
29
  setPublicOnly(publicOnly);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamplay",
3
- "version": "0.5.0-alpha.30",
3
+ "version": "0.5.0-alpha.32",
4
4
  "description": "Full-stack signals ORM with multiplayer",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -26,6 +26,11 @@
26
26
  "teamplay-ts": "./src/connect/index.js",
27
27
  "default": "./dist/connect/index.js"
28
28
  },
29
+ "./config": {
30
+ "teamplay-ts": "./src/config.ts",
31
+ "types": "./dist/config.d.ts",
32
+ "default": "./dist/config.js"
33
+ },
29
34
  "./server": {
30
35
  "teamplay-ts": "./src/server.js",
31
36
  "types": "./dist/server.d.ts",
@@ -133,6 +138,5 @@
133
138
  "<rootDir>/test_client/helpers"
134
139
  ]
135
140
  },
136
- "license": "MIT",
137
- "gitHead": "f568bc4a6af329926d4265c542eb2994b365bc28"
141
+ "license": "MIT"
138
142
  }
@@ -1,3 +0,0 @@
1
- export function compatStartOnRoot($root: any, targetPath: any, ...depsAndGetter: any[]): any;
2
- export function compatStopOnRoot($root: any, targetPath: any): void;
3
- export function joinScopePath(scopePath: any, relativePath: any): string;
@@ -1,250 +0,0 @@
1
- import { observe, raw, unobserve } from '@nx-js/observer-util';
2
- import { getActiveDocOpContext } from '../Doc.js';
3
- import { getRoot } from "../Root.js";
4
- import { scheduleReaction } from '../batchScheduler.js';
5
- const START_REACTIONS = Symbol('compat start reactions');
6
- const SKIP_TICK = Symbol('compat start skip tick');
7
- export function compatStartOnRoot($root, targetPath, ...depsAndGetter) {
8
- if (!isRootSignal($root))
9
- throw Error('Signal.start() is only available on root signal');
10
- if (typeof targetPath !== 'string')
11
- throw Error('Signal.start() expects targetPath to be a string');
12
- if (depsAndGetter.length < 1) {
13
- throw Error('Signal.start() expects targetPath, dependencies, and a getter function');
14
- }
15
- const getter = depsAndGetter[depsAndGetter.length - 1];
16
- if (typeof getter !== 'function') {
17
- throw Error('Signal.start() expects the last argument to be a getter function');
18
- }
19
- const deps = depsAndGetter.slice(0, -1);
20
- const targetSegments = parsePathSegments(targetPath);
21
- const $target = resolveSignal($root, targetSegments);
22
- const targetKey = $target.path();
23
- const depPaths = deps
24
- .map(dep => getStartDepPath(dep, $root))
25
- .filter(Boolean);
26
- const store = getStartStore($root);
27
- const existing = store.get(targetKey);
28
- if (existing)
29
- existing.stop();
30
- let lastSourceSnapshot = UNSET;
31
- let lastTargetSnapshot = UNSET;
32
- const reaction = observe(() => {
33
- const resolvedDeps = [];
34
- for (const dep of deps) {
35
- const resolved = resolveStartDep(dep, $root);
36
- if (resolved === SKIP_TICK)
37
- return;
38
- resolvedDeps.push(resolved);
39
- }
40
- let nextValue;
41
- try {
42
- nextValue = getter(...resolvedDeps);
43
- }
44
- catch (err) {
45
- if (isThenable(err))
46
- return;
47
- throw err;
48
- }
49
- const sourceSnapshot = detachStartValue(nextValue);
50
- if (lastSourceSnapshot !== UNSET && deepEqualStartValue(lastSourceSnapshot, sourceSnapshot)) {
51
- return;
52
- }
53
- const currentTargetSnapshot = lastTargetSnapshot === UNSET
54
- ? UNSET
55
- : detachStartValue($target.peek());
56
- if (currentTargetSnapshot !== UNSET && deepEqualStartValue(currentTargetSnapshot, sourceSnapshot)) {
57
- lastSourceSnapshot = sourceSnapshot;
58
- lastTargetSnapshot = sourceSnapshot;
59
- return;
60
- }
61
- if (currentTargetSnapshot !== UNSET &&
62
- !deepEqualStartValue(lastTargetSnapshot, currentTargetSnapshot) &&
63
- isActiveLocalDocOpForDeps(depPaths)) {
64
- lastSourceSnapshot = sourceSnapshot;
65
- return;
66
- }
67
- lastSourceSnapshot = sourceSnapshot;
68
- const detachedValue = detachStartValue(sourceSnapshot);
69
- lastTargetSnapshot = detachStartValue(detachedValue);
70
- // Keep the detached snapshot to avoid aliasing source and target.
71
- // Old racer start() writes through diffDeep by default. In compat mode we must preserve
72
- // that behavior, but also avoid reading the target reactively inside start(), otherwise
73
- // start() subscribes to its own output and local child edits get immediately overwritten.
74
- const maybePromise = $target.setDiffDeep(detachedValue);
75
- if (maybePromise?.then) {
76
- maybePromise
77
- .then(() => { })
78
- .catch(ignorePromiseRejection);
79
- }
80
- }, { scheduler: scheduleReaction });
81
- store.set(targetKey, { stop: () => unobserve(reaction) });
82
- return $target;
83
- }
84
- export function compatStopOnRoot($root, targetPath) {
85
- if (!isRootSignal($root))
86
- throw Error('Signal.stop() is only available on root signal');
87
- if (typeof targetPath !== 'string')
88
- throw Error('Signal.stop() expects targetPath to be a string');
89
- const targetSegments = parsePathSegments(targetPath);
90
- const $target = resolveSignal($root, targetSegments);
91
- const targetKey = $target.path();
92
- const store = getStartStore($root);
93
- const existing = store.get(targetKey);
94
- if (!existing)
95
- return;
96
- existing.stop();
97
- store.delete(targetKey);
98
- }
99
- export function joinScopePath(scopePath, relativePath) {
100
- if (typeof scopePath !== 'string')
101
- scopePath = '';
102
- const segments = [];
103
- if (scopePath)
104
- segments.push(...parsePathSegments(scopePath));
105
- if (relativePath)
106
- segments.push(...parsePathSegments(relativePath));
107
- return segments.join('.');
108
- }
109
- function getStartStore($root) {
110
- $root[START_REACTIONS] ??= new Map();
111
- return $root[START_REACTIONS];
112
- }
113
- function resolveStartDep(dep, $root) {
114
- try {
115
- if (isSignalLike(dep))
116
- return getStartDepValue(dep);
117
- if (typeof dep === 'string')
118
- return getStartDepValue(resolveSignal($root, parsePathSegments(dep)));
119
- return dep;
120
- }
121
- catch (err) {
122
- if (isThenable(err))
123
- return SKIP_TICK;
124
- throw err;
125
- }
126
- }
127
- function getStartDepPath(dep, $root) {
128
- if (isSignalLike(dep))
129
- return dep.path();
130
- if (typeof dep === 'string')
131
- return resolveSignal($root, parsePathSegments(dep)).path();
132
- }
133
- function getStartDepValue($signal) {
134
- return readReactiveSnapshot($signal.get());
135
- }
136
- function isActiveLocalDocOpForDeps(depPaths) {
137
- const context = getActiveDocOpContext();
138
- if (!context?.source)
139
- return false;
140
- const docPath = `${context.collection}.${context.docId}`;
141
- return depPaths.some(depPath => depPath === docPath || depPath.startsWith(docPath + '.'));
142
- }
143
- function readReactiveSnapshot(value) {
144
- if (!value || typeof value !== 'object')
145
- return value;
146
- if (value instanceof Date)
147
- return new Date(value);
148
- if (Array.isArray(value)) {
149
- const array = [];
150
- for (let i = 0; i < value.length; i++) {
151
- array[i] = readReactiveSnapshot(value[i]);
152
- }
153
- return array;
154
- }
155
- const object = new value.constructor();
156
- for (const key in value) {
157
- if (Object.prototype.hasOwnProperty.call(value, key)) {
158
- object[key] = readReactiveSnapshot(value[key]);
159
- }
160
- }
161
- return object;
162
- }
163
- function isSignalLike(value) {
164
- return value && typeof value.path === 'function' && typeof value.get === 'function';
165
- }
166
- function parsePathSegments(path) {
167
- return path.split('.').filter(Boolean);
168
- }
169
- function resolveSignal($base, segments) {
170
- let $cursor = $base;
171
- for (const segment of segments)
172
- $cursor = $cursor[segment];
173
- return $cursor;
174
- }
175
- function isRootSignal($signal) {
176
- return getRoot($signal) === $signal;
177
- }
178
- function ignorePromiseRejection() { }
179
- function isThenable(value) {
180
- return !!value && typeof value.then === 'function';
181
- }
182
- const UNSET = Symbol('compat start unset');
183
- function detachStartValue(value) {
184
- const rawValue = raw(value);
185
- if (!rawValue || typeof rawValue !== 'object')
186
- return rawValue;
187
- if (typeof globalThis.structuredClone === 'function') {
188
- try {
189
- return globalThis.structuredClone(rawValue);
190
- }
191
- catch { }
192
- }
193
- return racerDeepCopy(rawValue);
194
- }
195
- function racerDeepCopy(value) {
196
- if (value instanceof Date)
197
- return new Date(value);
198
- if (typeof value === 'object') {
199
- if (value === null)
200
- return null;
201
- if (Array.isArray(value)) {
202
- const array = [];
203
- for (let i = value.length; i--;) {
204
- array[i] = racerDeepCopy(value[i]);
205
- }
206
- return array;
207
- }
208
- const object = new value.constructor();
209
- for (const key in value) {
210
- if (Object.prototype.hasOwnProperty.call(value, key)) {
211
- object[key] = racerDeepCopy(value[key]);
212
- }
213
- }
214
- return object;
215
- }
216
- return value;
217
- }
218
- function deepEqualStartValue(left, right) {
219
- if (left === right)
220
- return true;
221
- if (Number.isNaN(left) && Number.isNaN(right))
222
- return true;
223
- if (left instanceof Date || right instanceof Date) {
224
- return left instanceof Date && right instanceof Date && left.getTime() === right.getTime();
225
- }
226
- if (!left || !right || typeof left !== 'object' || typeof right !== 'object')
227
- return false;
228
- if (Array.isArray(left) || Array.isArray(right)) {
229
- if (!Array.isArray(left) || !Array.isArray(right))
230
- return false;
231
- if (left.length !== right.length)
232
- return false;
233
- for (let i = 0; i < left.length; i++) {
234
- if (!deepEqualStartValue(left[i], right[i]))
235
- return false;
236
- }
237
- return true;
238
- }
239
- const leftKeys = Object.keys(left);
240
- const rightKeys = Object.keys(right);
241
- if (leftKeys.length !== rightKeys.length)
242
- return false;
243
- for (const key of leftKeys) {
244
- if (!Object.prototype.hasOwnProperty.call(right, key))
245
- return false;
246
- if (!deepEqualStartValue(left[key], right[key]))
247
- return false;
248
- }
249
- return true;
250
- }