@vaadin/hilla-react-signals 24.9.2 → 25.0.0-alpha10

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.
@@ -1,6 +1,6 @@
1
1
  import type { ConnectClient, EndpointRequestInit, Subscription } from '@vaadin/hilla-frontend';
2
+ import { type SignalCommand } from './commands.js';
2
3
  import { Signal } from './core.js';
3
- import { type StateEvent } from './events.js';
4
4
  export interface Operation {
5
5
  result: Promise<void>;
6
6
  }
@@ -15,27 +15,22 @@ export type ServerConnectionConfig = Readonly<{
15
15
  endpoint: string;
16
16
  method: string;
17
17
  params?: Record<string, unknown>;
18
- parentClientSignalId?: string;
19
18
  }>;
20
19
  declare class ServerConnection {
21
20
  #private;
22
21
  readonly config: ServerConnectionConfig;
23
22
  constructor(id: string, config: ServerConnectionConfig);
24
23
  get subscription(): Subscription<Readonly<{
25
- id: string;
26
- type: string;
27
- value: unknown;
28
- accepted: boolean;
29
- parentSignalId?: string;
24
+ commandId: import("./commands.js").Id;
25
+ targetNodeId: import("./commands.js").Id;
26
+ '@type': string;
30
27
  }>> | undefined;
31
28
  connect(): Subscription<Readonly<{
32
- id: string;
33
- type: string;
34
- value: unknown;
35
- accepted: boolean;
36
- parentSignalId?: string;
29
+ commandId: import("./commands.js").Id;
30
+ targetNodeId: import("./commands.js").Id;
31
+ '@type': string;
37
32
  }>>;
38
- update(event: StateEvent, init?: EndpointRequestInit): Promise<void>;
33
+ update(command: SignalCommand, init?: EndpointRequestInit): Promise<void>;
39
34
  disconnect(): void;
40
35
  }
41
36
  export declare const $update: unique symbol;
@@ -49,14 +44,15 @@ export declare abstract class FullStackSignal<T> extends DependencyTrackingSigna
49
44
  readonly server: ServerConnection;
50
45
  readonly pending: import("@preact/signals-core").ReadonlySignal<boolean>;
51
46
  readonly error: import("@preact/signals-core").ReadonlySignal<Error | undefined>;
52
- constructor(value: T | undefined, config: ServerConnectionConfig, id?: string);
47
+ protected readonly parent?: FullStackSignal<any>;
48
+ constructor(value: T | undefined, config: ServerConnectionConfig, id?: string, parent?: FullStackSignal<any>);
53
49
  protected [$createOperation]({ id, promise }: {
54
50
  id?: string;
55
51
  promise?: Promise<void>;
56
52
  }): Operation;
57
53
  protected [$setValueQuietly](value: T): void;
58
- protected [$update](event: StateEvent): Promise<void>;
59
- protected [$resolveOperation](eventId: string, reason?: string): void;
60
- protected abstract [$processServerResponse](event: StateEvent): void;
54
+ protected [$update](command: SignalCommand): Promise<void>;
55
+ protected [$resolveOperation](commandId: string, reason?: string): void;
56
+ protected abstract [$processServerResponse](command: SignalCommand): void;
61
57
  }
62
58
  export {};
@@ -1,6 +1,6 @@
1
- import { nanoid } from 'nanoid';
1
+ import { createSetCommand } from './commands.js';
2
2
  import { computed, signal, Signal } from './core.js';
3
- import { createSetStateEvent } from './events.js';
3
+ import { randomId } from './utils.js';
4
4
  const ENDPOINT = 'SignalsHandler';
5
5
  export class DependencyTrackingSignal extends Signal {
6
6
  #onFirstSubscribe;
@@ -38,24 +38,23 @@ class ServerConnection {
38
38
  return this.#subscription;
39
39
  }
40
40
  connect() {
41
- const { client, endpoint, method, params, parentClientSignalId } = this.config;
41
+ const { client, endpoint, method, params } = this.config;
42
42
  this.#subscription ??= client.subscribe(ENDPOINT, 'subscribe', {
43
43
  providerEndpoint: endpoint,
44
44
  providerMethod: method,
45
45
  clientSignalId: this.#id,
46
46
  params,
47
- parentClientSignalId,
48
47
  });
49
48
  return this.#subscription;
50
49
  }
51
- async update(event, init) {
50
+ async update(command, init) {
52
51
  const onTheFly = !this.#subscription;
53
52
  if (onTheFly) {
54
53
  this.connect();
55
54
  }
56
55
  await this.config.client.call(ENDPOINT, 'update', {
57
56
  clientSignalId: this.#id,
58
- event,
57
+ command,
59
58
  }, init ?? { mute: true });
60
59
  if (onTheFly) {
61
60
  this.disconnect();
@@ -79,16 +78,17 @@ export class FullStackSignal extends DependencyTrackingSignal {
79
78
  #pending = signal(false);
80
79
  #error = signal(undefined);
81
80
  #paused = true;
82
- constructor(value, config, id) {
83
- super(value, () => this.#connect(), () => this.#disconnect());
84
- this.id = id ?? nanoid();
81
+ parent;
82
+ constructor(value, config, id, parent) {
83
+ super(value, () => (!parent ? this.#connect() : undefined), () => (!parent ? this.#disconnect() : undefined));
84
+ this.id = id ?? randomId();
85
85
  this.server = new ServerConnection(this.id, config);
86
+ this.parent = parent;
86
87
  this.subscribe((v) => {
87
88
  if (!this.#paused) {
88
89
  this.#pending.value = true;
89
90
  this.#error.value = undefined;
90
- const signalId = config.parentClientSignalId !== undefined ? this.id : undefined;
91
- this[$update](createSetStateEvent(v, signalId, config.parentClientSignalId));
91
+ this[$update](createSetCommand('', v));
92
92
  }
93
93
  });
94
94
  this.#paused = false;
@@ -101,9 +101,9 @@ export class FullStackSignal extends DependencyTrackingSignal {
101
101
  promises.push(promise);
102
102
  }
103
103
  if (id) {
104
- promises.push(new Promise((resolve, reject) => {
105
- thens.set(id, { resolve, reject });
106
- }));
104
+ const { promise: p, resolve, reject } = Promise.withResolvers();
105
+ promises.push(p);
106
+ thens.set(id, { resolve, reject });
107
107
  }
108
108
  if (promises.length === 0) {
109
109
  promises.push(Promise.resolve());
@@ -123,9 +123,13 @@ export class FullStackSignal extends DependencyTrackingSignal {
123
123
  super.value = value;
124
124
  this.#paused = false;
125
125
  }
126
- async [$update](event) {
126
+ async [$update](command) {
127
+ if (this.parent) {
128
+ const routedCommand = { ...command, targetNodeId: this.id };
129
+ return this.parent[$update](routedCommand);
130
+ }
127
131
  return this.server
128
- .update(event)
132
+ .update(command)
129
133
  .catch((error) => {
130
134
  this.#error.value = error instanceof Error ? error : new Error(String(error));
131
135
  })
@@ -133,10 +137,10 @@ export class FullStackSignal extends DependencyTrackingSignal {
133
137
  this.#pending.value = false;
134
138
  });
135
139
  }
136
- [$resolveOperation](eventId, reason) {
137
- const operationPromise = this.#operationPromises.get(eventId);
140
+ [$resolveOperation](commandId, reason) {
141
+ const operationPromise = this.#operationPromises.get(commandId);
138
142
  if (operationPromise) {
139
- this.#operationPromises.delete(eventId);
143
+ this.#operationPromises.delete(commandId);
140
144
  if (reason) {
141
145
  operationPromise.reject(reason);
142
146
  }
@@ -149,9 +153,9 @@ export class FullStackSignal extends DependencyTrackingSignal {
149
153
  this.server
150
154
  .connect()
151
155
  .onSubscriptionLost(() => 'resubscribe')
152
- .onNext((event) => {
156
+ .onNext((command) => {
153
157
  this.#paused = true;
154
- this[$processServerResponse](event);
158
+ this[$processServerResponse](command);
155
159
  this.#paused = false;
156
160
  });
157
161
  }
@@ -1 +1 @@
1
- {"version":3,"file":"FullStackSignal.js","sourceRoot":"","sources":["src/FullStackSignal.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n ActionOnLostSubscription,\n ConnectClient,\n EndpointRequestInit,\n Subscription,\n} from '@vaadin/hilla-frontend';\nimport { nanoid } from 'nanoid';\nimport { computed, signal, Signal } from './core.js';\nimport { createSetStateEvent, type StateEvent } from './events.js';\n\nconst ENDPOINT = 'SignalsHandler';\n\n/**\n * A return type for signal operations that exposes a `result` property of type\n * `Promise`, that resolves when the operation is completed. It allows defining\n * callbacks to be run after the operation is completed, or error handling when\n * the operation fails.\n *\n * @example\n * ```ts\n * const sharedName = NameService.sharedName({ defaultValue: '' });\n * sharedName.replace('John').result\n * .then(() => console.log('Name updated successfully'))\n * .catch((error) => console.error('Failed to update the name:', error));\n * ```\n */\nexport interface Operation {\n result: Promise<void>;\n}\n\n/**\n * An abstraction of a signal that tracks the number of subscribers, and calls\n * the provided `onSubscribe` and `onUnsubscribe` callbacks for the first\n * subscription and the last unsubscription, respectively.\n * @internal\n */\nexport abstract class DependencyTrackingSignal<T> extends Signal<T> {\n readonly #onFirstSubscribe: () => void;\n readonly #onLastUnsubscribe: () => void;\n\n // -1 means to ignore the first subscription that is created internally in the\n // FullStackSignal constructor.\n #subscribeCount = -1;\n\n protected constructor(value: T | undefined, onFirstSubscribe: () => void, onLastUnsubscribe: () => void) {\n super(value);\n this.#onFirstSubscribe = onFirstSubscribe;\n this.#onLastUnsubscribe = onLastUnsubscribe;\n }\n\n protected override S(node: unknown): void {\n super.S(node);\n if (this.#subscribeCount === 0) {\n this.#onFirstSubscribe();\n }\n this.#subscribeCount += 1;\n }\n\n protected override U(node: unknown): void {\n super.U(node);\n this.#subscribeCount -= 1;\n if (this.#subscribeCount === 0) {\n this.#onLastUnsubscribe();\n }\n }\n}\n\n/**\n * An object that describes a data object to connect to the signal provider\n * service.\n */\nexport type ServerConnectionConfig = Readonly<{\n /**\n * The client instance to be used for communication.\n */\n client: ConnectClient;\n\n /**\n * The name of the signal provider service endpoint.\n */\n endpoint: string;\n\n /**\n * The name of the signal provider service method.\n */\n method: string;\n\n /**\n * Optional object with method call arguments to be sent to the endpoint\n * method that provides the signal when subscribing to it.\n */\n params?: Record<string, unknown>;\n\n /**\n * The unique identifier of the parent signal in the client.\n */\n parentClientSignalId?: string;\n}>;\n\n/**\n * A server connection manager.\n */\nclass ServerConnection {\n readonly #id: string;\n readonly config: ServerConnectionConfig;\n #subscription?: Subscription<StateEvent>;\n\n constructor(id: string, config: ServerConnectionConfig) {\n this.config = config;\n this.#id = id;\n }\n\n get subscription() {\n return this.#subscription;\n }\n\n connect() {\n const { client, endpoint, method, params, parentClientSignalId } = this.config;\n\n this.#subscription ??= client.subscribe(ENDPOINT, 'subscribe', {\n providerEndpoint: endpoint,\n providerMethod: method,\n clientSignalId: this.#id,\n params,\n parentClientSignalId,\n });\n\n return this.#subscription;\n }\n\n async update(event: StateEvent, init?: EndpointRequestInit): Promise<void> {\n const onTheFly = !this.#subscription;\n\n if (onTheFly) {\n this.connect();\n }\n\n await this.config.client.call(\n ENDPOINT,\n 'update',\n {\n clientSignalId: this.#id,\n event,\n },\n init ?? { mute: true },\n );\n\n if (onTheFly) {\n this.disconnect();\n }\n }\n\n disconnect() {\n this.#subscription?.cancel();\n this.#subscription = undefined;\n }\n}\n\nexport const $update = Symbol('update');\nexport const $processServerResponse = Symbol('processServerResponse');\nexport const $setValueQuietly = Symbol('setValueQuietly');\nexport const $resolveOperation = Symbol('resolveOperation');\nexport const $createOperation = Symbol('createOperation');\n\n/**\n * A signal that holds a shared value. Each change to the value is propagated to\n * the server-side signal provider. At the same time, each change received from\n * the server-side signal provider is propagated to the local signal and it's\n * subscribers.\n *\n * @internal\n */\nexport abstract class FullStackSignal<T> extends DependencyTrackingSignal<T> {\n /**\n * The unique identifier of the signal necessary to communicate with the\n * server.\n */\n readonly id: string;\n\n /**\n * The server connection manager.\n */\n readonly server: ServerConnection;\n\n /**\n * Defines whether the signal is currently awaits a server-side response.\n */\n readonly pending = computed(() => this.#pending.value);\n\n /**\n * Defines whether the signal has an error.\n */\n readonly error = computed(() => this.#error.value);\n\n readonly #pending = signal(false);\n readonly #error = signal<Error | undefined>(undefined);\n\n // Paused at the very start to prevent the signal from sending the initial\n // value to the server.\n #paused = true;\n\n constructor(value: T | undefined, config: ServerConnectionConfig, id?: string) {\n super(\n value,\n () => this.#connect(),\n () => this.#disconnect(),\n );\n this.id = id ?? nanoid();\n this.server = new ServerConnection(this.id, config);\n\n this.subscribe((v) => {\n if (!this.#paused) {\n this.#pending.value = true;\n this.#error.value = undefined;\n // For internal signals, the provided non-null to the constructor should\n // be used along with the parent client side signal id when sending the\n // set event to the server. For internal signals this combination is\n // needed for addressing the correct parent/child signal instances on\n // the server. For a standalone signal, both of them should be passed in\n // as undefined:\n const signalId = config.parentClientSignalId !== undefined ? this.id : undefined;\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this[$update](createSetStateEvent(v, signalId, config.parentClientSignalId));\n }\n });\n\n this.#paused = false;\n }\n\n // stores the promise handlers associated to operations\n readonly #operationPromises = new Map<\n string,\n {\n resolve(value: PromiseLike<void> | void): void;\n reject(reason?: any): void;\n }\n >();\n\n // creates the object to be returned by operations to allow defining callbacks\n protected [$createOperation]({ id, promise }: { id?: string; promise?: Promise<void> }): Operation {\n const thens = this.#operationPromises;\n const promises: Array<Promise<void>> = [];\n\n if (promise) {\n // Add the provided promise to the list of promises\n promises.push(promise);\n }\n\n if (id) {\n // Create a promise to be associated to the provided id\n promises.push(\n new Promise<void>((resolve, reject) => {\n thens.set(id, { resolve, reject });\n }),\n );\n }\n\n if (promises.length === 0) {\n // If no promises were added, return a resolved promise\n promises.push(Promise.resolve());\n }\n\n return {\n result: Promise.allSettled(promises).then((results) => {\n const lastResult = results[results.length - 1];\n if (lastResult.status === 'fulfilled') {\n return undefined;\n }\n throw lastResult.reason;\n }),\n };\n }\n\n /**\n * Sets the local value of the signal without sending any events to the server\n * @param value - The new value.\n * @internal\n */\n protected [$setValueQuietly](value: T): void {\n this.#paused = true;\n super.value = value;\n this.#paused = false;\n }\n\n /**\n * A method to update the server with the new value.\n *\n * @param event - The event to update the server with.\n * @returns The server response promise.\n */\n protected async [$update](event: StateEvent): Promise<void> {\n return this.server\n .update(event)\n .catch((error: unknown) => {\n this.#error.value = error instanceof Error ? error : new Error(String(error));\n })\n .finally(() => {\n this.#pending.value = false;\n });\n }\n\n /**\n * Resolves the operation promise associated with the given event id.\n *\n * @param eventId - The event id.\n * @param reason - The reason to reject the promise (if any).\n */\n protected [$resolveOperation](eventId: string, reason?: string): void {\n const operationPromise = this.#operationPromises.get(eventId);\n if (operationPromise) {\n this.#operationPromises.delete(eventId);\n if (reason) {\n operationPromise.reject(reason);\n } else {\n operationPromise.resolve();\n }\n }\n }\n\n /**\n * A method with to process the server response. The implementation is\n * specific for each signal type.\n *\n * @param event - The server response event.\n */\n protected abstract [$processServerResponse](event: StateEvent): void;\n\n #connect() {\n this.server\n .connect()\n .onSubscriptionLost(() => 'resubscribe' as ActionOnLostSubscription)\n .onNext((event: StateEvent) => {\n this.#paused = true;\n this[$processServerResponse](event);\n this.#paused = false;\n });\n }\n\n #disconnect() {\n if (this.server.subscription === undefined) {\n return;\n }\n this.server.disconnect();\n }\n}\n"]}
1
+ {"version":3,"file":"FullStackSignal.js","sourceRoot":"","sources":["src/FullStackSignal.ts"],"names":[],"mappings":"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","sourcesContent":["import type {\n ActionOnLostSubscription,\n ConnectClient,\n EndpointRequestInit,\n Subscription,\n} from '@vaadin/hilla-frontend';\nimport { createSetCommand, type SignalCommand } from './commands.js';\nimport { computed, signal, Signal } from './core.js';\nimport { randomId } from './utils.js';\n\nconst ENDPOINT = 'SignalsHandler';\n\n/**\n * A return type for signal operations that exposes a `result` property of type\n * `Promise`, that resolves when the operation is completed. It allows defining\n * callbacks to be run after the operation is completed, or error handling when\n * the operation fails.\n *\n * @example\n * ```ts\n * const sharedName = NameService.sharedName({ defaultValue: '' });\n * sharedName.replace('John').result\n * .then(() => console.log('Name updated successfully'))\n * .catch((error) => console.error('Failed to update the name:', error));\n * ```\n */\nexport interface Operation {\n result: Promise<void>;\n}\n\n/**\n * An abstraction of a signal that tracks the number of subscribers, and calls\n * the provided `onSubscribe` and `onUnsubscribe` callbacks for the first\n * subscription and the last unsubscription, respectively.\n * @internal\n */\nexport abstract class DependencyTrackingSignal<T> extends Signal<T> {\n readonly #onFirstSubscribe: () => void;\n readonly #onLastUnsubscribe: () => void;\n\n // -1 means to ignore the first subscription that is created internally in the\n // FullStackSignal constructor.\n #subscribeCount = -1;\n\n protected constructor(value: T | undefined, onFirstSubscribe: () => void, onLastUnsubscribe: () => void) {\n super(value);\n this.#onFirstSubscribe = onFirstSubscribe;\n this.#onLastUnsubscribe = onLastUnsubscribe;\n }\n\n protected override S(node: unknown): void {\n super.S(node);\n if (this.#subscribeCount === 0) {\n this.#onFirstSubscribe();\n }\n this.#subscribeCount += 1;\n }\n\n protected override U(node: unknown): void {\n super.U(node);\n this.#subscribeCount -= 1;\n if (this.#subscribeCount === 0) {\n this.#onLastUnsubscribe();\n }\n }\n}\n\n/**\n * An object that describes a data object to connect to the signal provider\n * service.\n */\nexport type ServerConnectionConfig = Readonly<{\n /**\n * The client instance to be used for communication.\n */\n client: ConnectClient;\n\n /**\n * The name of the signal provider service endpoint.\n */\n endpoint: string;\n\n /**\n * The name of the signal provider service method.\n */\n method: string;\n\n /**\n * Optional object with method call arguments to be sent to the endpoint\n * method that provides the signal when subscribing to it.\n */\n params?: Record<string, unknown>;\n}>;\n\n/**\n * A server connection manager.\n */\nclass ServerConnection {\n readonly #id: string;\n readonly config: ServerConnectionConfig;\n #subscription?: Subscription<SignalCommand>;\n\n constructor(id: string, config: ServerConnectionConfig) {\n this.config = config;\n this.#id = id;\n }\n\n get subscription() {\n return this.#subscription;\n }\n\n connect() {\n const { client, endpoint, method, params } = this.config;\n\n this.#subscription ??= client.subscribe(ENDPOINT, 'subscribe', {\n providerEndpoint: endpoint,\n providerMethod: method,\n clientSignalId: this.#id,\n params,\n });\n\n return this.#subscription;\n }\n\n async update(command: SignalCommand, init?: EndpointRequestInit): Promise<void> {\n const onTheFly = !this.#subscription;\n\n if (onTheFly) {\n this.connect();\n }\n\n await this.config.client.call(\n ENDPOINT,\n 'update',\n {\n clientSignalId: this.#id,\n command,\n },\n init ?? { mute: true },\n );\n\n if (onTheFly) {\n this.disconnect();\n }\n }\n\n disconnect() {\n this.#subscription?.cancel();\n this.#subscription = undefined;\n }\n}\n\nexport const $update = Symbol('update');\nexport const $processServerResponse = Symbol('processServerResponse');\nexport const $setValueQuietly = Symbol('setValueQuietly');\nexport const $resolveOperation = Symbol('resolveOperation');\nexport const $createOperation = Symbol('createOperation');\n\n/**\n * A signal that holds a shared value. Each change to the value is propagated to\n * the server-side signal provider. At the same time, each change received from\n * the server-side signal provider is propagated to the local signal and it's\n * subscribers.\n *\n * @internal\n */\nexport abstract class FullStackSignal<T> extends DependencyTrackingSignal<T> {\n /**\n * The unique identifier of the signal necessary to communicate with the\n * server.\n */\n readonly id: string;\n\n /**\n * The server connection manager.\n */\n readonly server: ServerConnection;\n\n /**\n * Defines whether the signal is currently awaits a server-side response.\n */\n readonly pending = computed(() => this.#pending.value);\n\n /**\n * Defines whether the signal has an error.\n */\n readonly error = computed(() => this.#error.value);\n\n readonly #pending = signal(false);\n readonly #error = signal<Error | undefined>(undefined);\n\n // Paused at the very start to prevent the signal from sending the initial\n // value to the server.\n #paused = true;\n\n /**\n * Optional parent signal for command routing.\n */\n protected readonly parent?: FullStackSignal<any>;\n\n constructor(value: T | undefined, config: ServerConnectionConfig, id?: string, parent?: FullStackSignal<any>) {\n super(\n value,\n () => (!parent ? this.#connect() : undefined),\n () => (!parent ? this.#disconnect() : undefined),\n );\n this.id = id ?? randomId();\n this.server = new ServerConnection(this.id, config);\n this.parent = parent;\n\n this.subscribe((v) => {\n if (!this.#paused) {\n this.#pending.value = true;\n this.#error.value = undefined;\n // eslint-disable-next-line @typescript-eslint/no-floating-promises\n this[$update](createSetCommand('', v));\n }\n });\n\n this.#paused = false;\n }\n\n // stores the promise handlers associated to operations\n readonly #operationPromises = new Map<\n string,\n {\n resolve(value: PromiseLike<void> | void): void;\n reject(reason?: any): void;\n }\n >();\n\n // creates the object to be returned by operations to allow defining callbacks\n protected [$createOperation]({ id, promise }: { id?: string; promise?: Promise<void> }): Operation {\n const thens = this.#operationPromises;\n const promises: Array<Promise<void>> = [];\n\n if (promise) {\n // Add the provided promise to the list of promises\n promises.push(promise);\n }\n\n if (id) {\n // eslint-disable-next-line @typescript-eslint/unbound-method\n const { promise: p, resolve, reject } = Promise.withResolvers<void>();\n\n // Create a promise to be associated to the provided id\n promises.push(p);\n thens.set(id, { resolve, reject });\n }\n\n if (promises.length === 0) {\n // If no promises were added, return a resolved promise\n promises.push(Promise.resolve());\n }\n\n return {\n result: Promise.allSettled(promises).then((results) => {\n const lastResult = results[results.length - 1];\n if (lastResult.status === 'fulfilled') {\n return undefined;\n }\n throw lastResult.reason;\n }),\n };\n }\n\n /**\n * Sets the local value of the signal without sending any events to the server\n * @param value - The new value.\n * @internal\n */\n protected [$setValueQuietly](value: T): void {\n this.#paused = true;\n super.value = value;\n this.#paused = false;\n }\n\n /**\n * A method to update the server with the new value or via parent.\n *\n * @param command - The command to update the server with.\n * @returns The server response promise.\n */\n protected async [$update](command: SignalCommand): Promise<void> {\n if (this.parent) {\n // Route command via parent\n const routedCommand = { ...command, targetNodeId: this.id };\n return this.parent[$update](routedCommand);\n }\n return this.server\n .update(command)\n .catch((error: unknown) => {\n this.#error.value = error instanceof Error ? error : new Error(String(error));\n })\n .finally(() => {\n this.#pending.value = false;\n });\n }\n\n /**\n * Resolves the operation promise associated with the given event id.\n *\n * @param commandId - The command id.\n * @param reason - The reason to reject the promise (if any).\n */\n protected [$resolveOperation](commandId: string, reason?: string): void {\n const operationPromise = this.#operationPromises.get(commandId);\n if (operationPromise) {\n this.#operationPromises.delete(commandId);\n if (reason) {\n operationPromise.reject(reason);\n } else {\n operationPromise.resolve();\n }\n }\n }\n\n /**\n * A method with to process the server response. The implementation is\n * specific for each signal type.\n *\n * @param command - The server response command.\n */\n protected abstract [$processServerResponse](command: SignalCommand): void;\n\n #connect() {\n this.server\n .connect()\n .onSubscriptionLost(() => 'resubscribe' as ActionOnLostSubscription)\n .onNext((command: SignalCommand) => {\n this.#paused = true;\n this[$processServerResponse](command);\n this.#paused = false;\n });\n }\n\n #disconnect() {\n if (this.server.subscription === undefined) {\n return;\n }\n this.server.disconnect();\n }\n}\n"]}
package/ListSignal.d.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import { CollectionSignal } from './CollectionSignal.js';
2
- import { type StateEvent } from './events.js';
2
+ import { ListPosition, type AdoptAtCommand, type InsertCommand, type PositionCondition, type RemoveCommand, type SetCommand, type SnapshotCommand } from './commands.js';
3
3
  import { $processServerResponse, type Operation, type ServerConnectionConfig } from './FullStackSignal.js';
4
4
  import { ValueSignal } from './ValueSignal.js';
5
- export declare class ListSignal<T> extends CollectionSignal<ReadonlyArray<ValueSignal<T>>> {
6
- #private;
7
- constructor(config: ServerConnectionConfig);
8
- protected [$processServerResponse](event: StateEvent): void;
5
+ export declare class ListSignal<T> extends CollectionSignal<Array<ValueSignal<T>>> {
6
+ constructor(config: ServerConnectionConfig, id?: string);
7
+ insertFirst(value: T): Operation;
9
8
  insertLast(value: T): Operation;
10
- remove(item: ValueSignal<T>): Operation;
9
+ insertAt(value: T, at: ListPosition): Operation;
10
+ remove(child: ValueSignal<T>): Operation;
11
+ protected [$processServerResponse](command: InsertCommand<T> | RemoveCommand | AdoptAtCommand | PositionCondition | SnapshotCommand | SetCommand<T>): void;
11
12
  }
package/ListSignal.js CHANGED
@@ -1,121 +1,108 @@
1
1
  import { CollectionSignal } from './CollectionSignal.js';
2
- import { createInsertLastStateEvent, createRemoveStateEvent, isInsertLastStateEvent, isListSnapshotStateEvent, isRemoveStateEvent, } from './events.js';
2
+ import { createInsertCommand, createRemoveCommand, isAdoptAtCommand, isInsertCommand, isPositionCondition, isRemoveCommand, isSetCommand, isSnapshotCommand, ListPosition, ZERO, } from './commands.js';
3
3
  import { $createOperation, $processServerResponse, $resolveOperation, $setValueQuietly, $update, } from './FullStackSignal.js';
4
4
  import { ValueSignal } from './ValueSignal.js';
5
5
  export class ListSignal extends CollectionSignal {
6
- #head;
7
- #tail;
8
- #values = new Map();
9
- constructor(config) {
10
- const initialValue = [];
11
- super(initialValue, config);
6
+ constructor(config, id) {
7
+ super([], config, id);
12
8
  }
13
- #computeItems() {
14
- let current = this.#head;
15
- const result = [];
16
- while (current !== undefined) {
17
- const entry = this.#values.get(current);
18
- result.push(entry.value);
19
- current = entry.next;
20
- }
21
- return result;
9
+ insertFirst(value) {
10
+ return this.insertAt(value, ListPosition.first());
22
11
  }
23
- [$processServerResponse](event) {
24
- if (!event.accepted) {
25
- this[$resolveOperation](event.id, `Server rejected the operation with id '${event.id}'. See the server log for more details.`);
26
- return;
27
- }
28
- if (isListSnapshotStateEvent(event)) {
29
- this.#handleSnapshotEvent(event);
30
- }
31
- else if (isInsertLastStateEvent(event)) {
32
- this.#handleInsertLastUpdate(event);
33
- }
34
- else if (isRemoveStateEvent(event)) {
35
- this.#handleRemoveUpdate(event);
36
- }
37
- this[$resolveOperation](event.id);
12
+ insertLast(value) {
13
+ return this.insertAt(value, ListPosition.last());
38
14
  }
39
- #handleInsertLastUpdate(event) {
40
- if (event.entryId === undefined) {
41
- throw new Error('Unexpected state: Entry id should be defined when insert last event is accepted');
42
- }
43
- const valueSignal = new ValueSignal(event.value, { ...this.server.config, parentClientSignalId: this.id }, event.entryId);
44
- const newEntry = { id: valueSignal.id, value: valueSignal };
45
- if (this.#head === undefined) {
46
- this.#head = newEntry.id;
47
- this.#tail = this.#head;
48
- }
49
- else {
50
- const tailEntry = this.#values.get(this.#tail);
51
- tailEntry.next = newEntry.id;
52
- newEntry.prev = this.#tail;
53
- this.#tail = newEntry.id;
54
- }
55
- this.#values.set(valueSignal.id, newEntry);
56
- this[$setValueQuietly](this.#computeItems());
15
+ insertAt(value, at) {
16
+ const command = createInsertCommand(ZERO, value, at);
17
+ const promise = this[$update](command);
18
+ return this[$createOperation]({ id: command.commandId, promise });
19
+ }
20
+ remove(child) {
21
+ const command = createRemoveCommand(child.id, ZERO);
22
+ const promise = this[$update](command);
23
+ return this[$createOperation]({ id: command.commandId, promise });
57
24
  }
58
- #handleRemoveUpdate(event) {
59
- const entryToRemove = this.#values.get(event.entryId);
60
- if (entryToRemove === undefined) {
61
- return;
25
+ [$processServerResponse](command) {
26
+ if ((isSnapshotCommand(command) || isSetCommand(command)) && command.targetNodeId) {
27
+ const targetChild = this.value.find((child) => child.id === command.targetNodeId);
28
+ if (targetChild) {
29
+ targetChild[$processServerResponse](command);
30
+ return;
31
+ }
62
32
  }
63
- this.#values.delete(event.id);
64
- if (this.#head === entryToRemove.id) {
65
- if (entryToRemove.next === undefined) {
66
- this.#head = undefined;
67
- this.#tail = undefined;
33
+ if (isInsertCommand(command)) {
34
+ const valueSignal = new ValueSignal(command.value, this.server.config, command.commandId, this);
35
+ let insertIndex = this.value.length;
36
+ const pos = command.position;
37
+ if (pos.after === '' && pos.before == null) {
38
+ insertIndex = 0;
68
39
  }
69
- else {
70
- const newHead = this.#values.get(entryToRemove.next);
71
- this.#head = newHead.id;
72
- newHead.prev = undefined;
40
+ else if (pos.after == null && pos.before === '') {
41
+ insertIndex = this.value.length;
73
42
  }
74
- }
75
- else {
76
- const prevEntry = this.#values.get(entryToRemove.prev);
77
- const nextEntry = entryToRemove.next !== undefined ? this.#values.get(entryToRemove.next) : undefined;
78
- if (nextEntry === undefined) {
79
- this.#tail = prevEntry.id;
80
- prevEntry.next = undefined;
43
+ else if (typeof pos.after === 'string' && pos.after !== '') {
44
+ const idx = this.value.findIndex((v) => v.id === pos.after);
45
+ insertIndex = idx !== -1 ? idx + 1 : this.value.length;
81
46
  }
82
- else {
83
- prevEntry.next = nextEntry.id;
84
- nextEntry.prev = prevEntry.id;
47
+ else if (typeof pos.before === 'string' && pos.before !== '') {
48
+ const idx = this.value.findIndex((v) => v.id === pos.before);
49
+ insertIndex = idx !== -1 ? idx : this.value.length;
85
50
  }
51
+ const newList = [...this.value.slice(0, insertIndex), valueSignal, ...this.value.slice(insertIndex)];
52
+ this[$setValueQuietly](newList);
53
+ this[$resolveOperation](command.commandId, undefined);
86
54
  }
87
- this[$setValueQuietly](this.#computeItems());
88
- }
89
- #handleSnapshotEvent(event) {
90
- event.entries.forEach((entry) => {
91
- this.#values.set(entry.id, {
92
- id: entry.id,
93
- prev: entry.prev,
94
- next: entry.next,
95
- value: new ValueSignal(entry.value, { ...this.server.config, parentClientSignalId: this.id }, entry.id),
96
- });
97
- if (entry.prev === undefined) {
98
- this.#head = entry.id;
55
+ else if (isRemoveCommand(command)) {
56
+ const removeIndex = this.value.findIndex((child) => child.id === command.targetNodeId);
57
+ if (removeIndex !== -1) {
58
+ const newList = [...this.value.slice(0, removeIndex), ...this.value.slice(removeIndex + 1)];
59
+ this[$setValueQuietly](newList);
99
60
  }
100
- if (entry.next === undefined) {
101
- this.#tail = entry.id;
61
+ this[$resolveOperation](command.commandId, undefined);
62
+ }
63
+ else if (isAdoptAtCommand(command)) {
64
+ const moveIndex = this.value.findIndex((child) => child.id === command.childId);
65
+ if (moveIndex !== -1) {
66
+ const [movedChild] = this.value.splice(moveIndex, 1);
67
+ let newIndex = this.value.length;
68
+ const pos = command.position;
69
+ if (pos.after === '' && pos.before == null) {
70
+ newIndex = 0;
71
+ }
72
+ else if (pos.after == null && pos.before === '') {
73
+ newIndex = this.value.length;
74
+ }
75
+ else if (typeof pos.after === 'string' && pos.after !== '') {
76
+ const idx = this.value.findIndex((v) => v.id === pos.after);
77
+ newIndex = idx !== -1 ? idx + 1 : this.value.length;
78
+ }
79
+ else if (typeof pos.before === 'string' && pos.before !== '') {
80
+ const idx = this.value.findIndex((v) => v.id === pos.before);
81
+ newIndex = idx !== -1 ? idx : this.value.length;
82
+ }
83
+ this.value.splice(newIndex, 0, movedChild);
102
84
  }
103
- });
104
- this[$setValueQuietly](this.#computeItems());
105
- }
106
- insertLast(value) {
107
- const event = createInsertLastStateEvent(value);
108
- const promise = this[$update](event);
109
- return this[$createOperation]({ id: event.id, promise });
110
- }
111
- remove(item) {
112
- const entryToRemove = this.#values.get(item.id);
113
- if (entryToRemove === undefined) {
114
- return { result: Promise.resolve() };
85
+ this[$resolveOperation](command.commandId, undefined);
86
+ }
87
+ else if (isPositionCondition(command)) {
88
+ this[$resolveOperation](command.commandId, undefined);
89
+ }
90
+ else if (isSnapshotCommand(command)) {
91
+ const { nodes } = command;
92
+ const listNode = nodes[''];
93
+ const childrenIds = listNode.listChildren;
94
+ const valueSignals = childrenIds
95
+ .map((childId) => {
96
+ const childNode = nodes[childId];
97
+ if ('value' in childNode) {
98
+ return new ValueSignal(childNode.value, this.server.config, childId, this);
99
+ }
100
+ return null;
101
+ })
102
+ .filter(Boolean);
103
+ this[$setValueQuietly](valueSignals);
104
+ this[$resolveOperation](command.commandId, undefined);
115
105
  }
116
- const removeEvent = createRemoveStateEvent(entryToRemove.value.id);
117
- const promise = this[$update](removeEvent);
118
- return this[$createOperation]({ id: removeEvent.id, promise });
119
106
  }
120
107
  }
121
108
  //# sourceMappingURL=ListSignal.js.map
package/ListSignal.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"ListSignal.js","sourceRoot":"","sources":["src/ListSignal.ts"],"names":[],"mappings":"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","sourcesContent":["import { CollectionSignal } from './CollectionSignal.js';\nimport {\n createInsertLastStateEvent,\n createRemoveStateEvent,\n type InsertLastStateEvent,\n isInsertLastStateEvent,\n isListSnapshotStateEvent,\n isRemoveStateEvent,\n type ListSnapshotStateEvent,\n type RemoveStateEvent,\n type StateEvent,\n} from './events.js';\nimport {\n $createOperation,\n $processServerResponse,\n $resolveOperation,\n $setValueQuietly,\n $update,\n type Operation,\n type ServerConnectionConfig,\n} from './FullStackSignal.js';\nimport { ValueSignal } from './ValueSignal.js';\n\ntype EntryId = string;\ntype Entry<T> = {\n id: EntryId;\n value: ValueSignal<T>;\n next?: EntryId;\n prev?: EntryId;\n};\n\n/**\n * A {@link FullStackSignal} that represents a shared list of values, where each\n * value is represented by a {@link ValueSignal}.\n * The list can be modified by calling the defined methods to insert or remove\n * items, but the `value` property of a `ListSignal` instance is read-only and\n * cannot be assigned directly.\n * The value of each item in the list can be manipulated similar to a regular\n * {@link ValueSignal}.\n *\n * @typeParam T - The type of the values in the list.\n */\nexport class ListSignal<T> extends CollectionSignal<ReadonlyArray<ValueSignal<T>>> {\n #head?: EntryId;\n #tail?: EntryId;\n\n readonly #values = new Map<string, Entry<T>>();\n\n constructor(config: ServerConnectionConfig) {\n const initialValue: Array<ValueSignal<T>> = [];\n super(initialValue, config);\n }\n\n #computeItems(): ReadonlyArray<ValueSignal<T>> {\n let current = this.#head;\n const result: Array<ValueSignal<T>> = [];\n while (current !== undefined) {\n const entry = this.#values.get(current)!;\n result.push(entry.value);\n current = entry.next;\n }\n return result;\n }\n\n protected override [$processServerResponse](event: StateEvent): void {\n if (!event.accepted) {\n this[$resolveOperation](\n event.id,\n `Server rejected the operation with id '${event.id}'. See the server log for more details.`,\n );\n return;\n }\n if (isListSnapshotStateEvent<T>(event)) {\n this.#handleSnapshotEvent(event);\n } else if (isInsertLastStateEvent<T>(event)) {\n this.#handleInsertLastUpdate(event);\n } else if (isRemoveStateEvent(event)) {\n this.#handleRemoveUpdate(event);\n }\n this[$resolveOperation](event.id);\n }\n\n #handleInsertLastUpdate(event: InsertLastStateEvent<T>): void {\n if (event.entryId === undefined) {\n throw new Error('Unexpected state: Entry id should be defined when insert last event is accepted');\n }\n const valueSignal = new ValueSignal<T>(\n event.value,\n { ...this.server.config, parentClientSignalId: this.id },\n event.entryId,\n );\n const newEntry: Entry<T> = { id: valueSignal.id, value: valueSignal };\n\n if (this.#head === undefined) {\n this.#head = newEntry.id;\n this.#tail = this.#head;\n } else {\n const tailEntry = this.#values.get(this.#tail!)!;\n tailEntry.next = newEntry.id;\n newEntry.prev = this.#tail;\n this.#tail = newEntry.id;\n }\n this.#values.set(valueSignal.id, newEntry);\n this[$setValueQuietly](this.#computeItems());\n }\n\n #handleRemoveUpdate(event: RemoveStateEvent): void {\n const entryToRemove = this.#values.get(event.entryId);\n if (entryToRemove === undefined) {\n return;\n }\n this.#values.delete(event.id);\n if (this.#head === entryToRemove.id) {\n if (entryToRemove.next === undefined) {\n this.#head = undefined;\n this.#tail = undefined;\n } else {\n const newHead = this.#values.get(entryToRemove.next)!;\n this.#head = newHead.id;\n newHead.prev = undefined;\n }\n } else {\n const prevEntry = this.#values.get(entryToRemove.prev!)!;\n const nextEntry = entryToRemove.next !== undefined ? this.#values.get(entryToRemove.next) : undefined;\n if (nextEntry === undefined) {\n this.#tail = prevEntry.id;\n prevEntry.next = undefined;\n } else {\n prevEntry.next = nextEntry.id;\n nextEntry.prev = prevEntry.id;\n }\n }\n this[$setValueQuietly](this.#computeItems());\n }\n\n #handleSnapshotEvent(event: ListSnapshotStateEvent<T>): void {\n event.entries.forEach((entry) => {\n this.#values.set(entry.id, {\n id: entry.id,\n prev: entry.prev,\n next: entry.next,\n value: new ValueSignal(entry.value, { ...this.server.config, parentClientSignalId: this.id }, entry.id),\n });\n if (entry.prev === undefined) {\n this.#head = entry.id;\n }\n if (entry.next === undefined) {\n this.#tail = entry.id;\n }\n });\n this[$setValueQuietly](this.#computeItems());\n }\n\n /**\n * Inserts a new value at the end of the list.\n * @param value - The value to insert.\n * @returns An operation object that allows to perform additional actions.\n */\n insertLast(value: T): Operation {\n const event = createInsertLastStateEvent(value);\n const promise = this[$update](event);\n return this[$createOperation]({ id: event.id, promise });\n }\n\n /**\n * Removes the given item from the list.\n * @param item - The item to remove.\n * @returns An operation object that allows to perform additional actions.\n */\n remove(item: ValueSignal<T>): Operation {\n const entryToRemove = this.#values.get(item.id);\n if (entryToRemove === undefined) {\n return { result: Promise.resolve() };\n }\n const removeEvent = createRemoveStateEvent(entryToRemove.value.id);\n const promise = this[$update](removeEvent);\n return this[$createOperation]({ id: removeEvent.id, promise });\n }\n}\n"]}
1
+ {"version":3,"file":"ListSignal.js","sourceRoot":"","sources":["src/ListSignal.ts"],"names":[],"mappings":"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","sourcesContent":["import { CollectionSignal } from './CollectionSignal.js';\nimport {\n createInsertCommand,\n createRemoveCommand,\n isAdoptAtCommand,\n isInsertCommand,\n isPositionCondition,\n isRemoveCommand,\n isSetCommand,\n isSnapshotCommand,\n ListPosition,\n ZERO,\n type AdoptAtCommand,\n type InsertCommand,\n type PositionCondition,\n type RemoveCommand,\n type SetCommand,\n type SnapshotCommand,\n} from './commands.js';\nimport {\n $createOperation,\n $processServerResponse,\n $resolveOperation,\n $setValueQuietly,\n $update,\n type Operation,\n type ServerConnectionConfig,\n} from './FullStackSignal.js';\nimport { ValueSignal } from './ValueSignal.js';\n\n/**\n * A signal containing a list of values. Supports atomic updates to the list structure.\n * Each value in the list is accessed as a separate ValueSignal instance.\n */\nexport class ListSignal<T> extends CollectionSignal<Array<ValueSignal<T>>> {\n constructor(config: ServerConnectionConfig, id?: string) {\n super([], config, id);\n }\n\n /**\n * Inserts a value as the first entry in this list.\n * @param value - The value to insert\n * @returns An operation containing a signal for the inserted entry and the eventual result\n */\n insertFirst(value: T): Operation {\n return this.insertAt(value, ListPosition.first());\n }\n\n /**\n * Inserts a value as the last entry in this list.\n * @param value - The value to insert\n * @returns An operation containing a signal for the inserted entry and the eventual result\n */\n insertLast(value: T): Operation {\n return this.insertAt(value, ListPosition.last());\n }\n\n /**\n * Inserts a value at the given position in this list.\n * @param value - The value to insert\n * @param at - The insert position\n * @returns An operation containing a signal for the inserted entry and the eventual result\n */\n insertAt(value: T, at: ListPosition): Operation {\n const command = createInsertCommand<T>(ZERO, value, at);\n const promise = this[$update](command);\n return this[$createOperation]({ id: command.commandId, promise });\n }\n\n /**\n * Removes the given child from this list.\n * @param child - The child to remove\n * @returns An operation containing the eventual result\n */\n remove(child: ValueSignal<T>): Operation {\n const command = createRemoveCommand(child.id, ZERO);\n const promise = this[$update](command);\n return this[$createOperation]({ id: command.commandId, promise });\n }\n\n protected override [$processServerResponse](\n command: InsertCommand<T> | RemoveCommand | AdoptAtCommand | PositionCondition | SnapshotCommand | SetCommand<T>,\n ): void {\n // Check if the command has a targetNodeId and reroute it to the corresponding child\n if ((isSnapshotCommand(command) || isSetCommand(command)) && command.targetNodeId) {\n const targetChild = this.value.find((child) => child.id === command.targetNodeId);\n\n if (targetChild) {\n targetChild[$processServerResponse](command);\n return;\n }\n }\n\n if (isInsertCommand<T>(command)) {\n const valueSignal = new ValueSignal<T>(command.value, this.server.config, command.commandId, this);\n let insertIndex = this.value.length;\n const pos = command.position;\n if (pos.after === '' && pos.before == null) {\n insertIndex = 0;\n } else if (pos.after == null && pos.before === '') {\n insertIndex = this.value.length;\n } else if (typeof pos.after === 'string' && pos.after !== '') {\n const idx = this.value.findIndex((v) => v.id === pos.after);\n insertIndex = idx !== -1 ? idx + 1 : this.value.length;\n } else if (typeof pos.before === 'string' && pos.before !== '') {\n const idx = this.value.findIndex((v) => v.id === pos.before);\n insertIndex = idx !== -1 ? idx : this.value.length;\n }\n const newList = [...this.value.slice(0, insertIndex), valueSignal, ...this.value.slice(insertIndex)];\n this[$setValueQuietly](newList);\n this[$resolveOperation](command.commandId, undefined);\n } else if (isRemoveCommand(command)) {\n const removeIndex = this.value.findIndex((child) => child.id === command.targetNodeId);\n if (removeIndex !== -1) {\n const newList = [...this.value.slice(0, removeIndex), ...this.value.slice(removeIndex + 1)];\n this[$setValueQuietly](newList);\n }\n this[$resolveOperation](command.commandId, undefined);\n } else if (isAdoptAtCommand(command)) {\n const moveIndex = this.value.findIndex((child) => child.id === command.childId);\n if (moveIndex !== -1) {\n const [movedChild] = this.value.splice(moveIndex, 1);\n let newIndex = this.value.length;\n const pos = command.position;\n if (pos.after === '' && pos.before == null) {\n newIndex = 0;\n } else if (pos.after == null && pos.before === '') {\n newIndex = this.value.length;\n } else if (typeof pos.after === 'string' && pos.after !== '') {\n const idx = this.value.findIndex((v) => v.id === pos.after);\n newIndex = idx !== -1 ? idx + 1 : this.value.length;\n } else if (typeof pos.before === 'string' && pos.before !== '') {\n const idx = this.value.findIndex((v) => v.id === pos.before);\n newIndex = idx !== -1 ? idx : this.value.length;\n }\n this.value.splice(newIndex, 0, movedChild);\n }\n this[$resolveOperation](command.commandId, undefined);\n } else if (isPositionCondition(command)) {\n this[$resolveOperation](command.commandId, undefined);\n } else if (isSnapshotCommand(command)) {\n const { nodes } = command;\n const listNode = nodes[''];\n\n const childrenIds = listNode.listChildren;\n const valueSignals = childrenIds\n .map((childId) => {\n const childNode = nodes[childId];\n if ('value' in childNode) {\n return new ValueSignal<T>(childNode.value as T, this.server.config, childId, this);\n }\n return null;\n })\n .filter(Boolean) as Array<ValueSignal<T>>;\n\n this[$setValueQuietly](valueSignals);\n this[$resolveOperation](command.commandId, undefined);\n }\n }\n}\n"]}
package/NumberSignal.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { type StateEvent } from './events.js';
1
+ import { type SignalCommand } from './commands.js';
2
2
  import { $processServerResponse, type Operation } from './FullStackSignal.js';
3
3
  import { ValueSignal } from './ValueSignal.js';
4
4
  export declare class NumberSignal extends ValueSignal<number> {
5
- #private;
6
5
  incrementBy(delta: number): Operation;
7
- protected [$processServerResponse](event: StateEvent): void;
6
+ valueAsInt(): number;
7
+ protected [$processServerResponse](command: SignalCommand): void;
8
8
  }
package/NumberSignal.js CHANGED
@@ -1,31 +1,26 @@
1
- import { createIncrementStateEvent, isIncrementStateEvent } from './events.js';
1
+ import { createIncrementCommand, isIncrementCommand } from './commands.js';
2
2
  import { $createOperation, $processServerResponse, $resolveOperation, $setValueQuietly, $update, } from './FullStackSignal.js';
3
3
  import { ValueSignal } from './ValueSignal.js';
4
4
  export class NumberSignal extends ValueSignal {
5
- #sentIncrementEvents = new Map();
6
5
  incrementBy(delta) {
7
6
  if (delta === 0) {
8
- return { result: Promise.resolve() };
7
+ const resolvedPromise = Promise.resolve(undefined);
8
+ return this[$createOperation]({ id: '', promise: resolvedPromise });
9
9
  }
10
- this[$setValueQuietly](this.value + delta);
11
- const event = createIncrementStateEvent(delta);
12
- this.#sentIncrementEvents.set(event.id, event);
13
- const promise = this[$update](event);
14
- return this[$createOperation]({ id: event.id, promise });
10
+ const command = createIncrementCommand('', delta);
11
+ const promise = this[$update](command);
12
+ return this[$createOperation]({ id: command.commandId, promise });
15
13
  }
16
- [$processServerResponse](event) {
17
- if (event.accepted && isIncrementStateEvent(event)) {
18
- const sentEvent = this.#sentIncrementEvents.get(event.id);
19
- if (sentEvent) {
20
- this.#sentIncrementEvents.delete(event.id);
21
- }
22
- else {
23
- this[$setValueQuietly](this.value + event.value);
24
- }
25
- this[$resolveOperation](event.id);
14
+ valueAsInt() {
15
+ return Math.trunc(this.value);
16
+ }
17
+ [$processServerResponse](command) {
18
+ if (isIncrementCommand(command)) {
19
+ this[$setValueQuietly](this.value + command.delta);
20
+ this[$resolveOperation](command.commandId, undefined);
26
21
  }
27
22
  else {
28
- super[$processServerResponse](event);
23
+ super[$processServerResponse](command);
29
24
  }
30
25
  }
31
26
  }
@@ -1 +1 @@
1
- {"version":3,"file":"NumberSignal.js","sourceRoot":"","sources":["src/NumberSignal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,yBAAyB,EAAE,qBAAqB,EAAmB,MAAM,aAAa,CAAC;AAChG,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,iBAAiB,EACjB,gBAAgB,EAChB,OAAO,GAER,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAyB/C,MAAM,OAAO,YAAa,SAAQ,WAAmB;IAC1C,oBAAoB,GAAG,IAAI,GAAG,EAAsB,CAAC;IAc9D,WAAW,CAAC,KAAa;QACvB,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;YAChB,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC;QACvC,CAAC;QACD,IAAI,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,CAAC;QAC3C,MAAM,KAAK,GAAG,yBAAyB,CAAC,KAAK,CAAC,CAAC;QAC/C,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;QAC/C,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC;QACrC,OAAO,IAAI,CAAC,gBAAgB,CAAC,CAAC,EAAE,EAAE,EAAE,KAAK,CAAC,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;IAC3D,CAAC;IAEkB,CAAC,sBAAsB,CAAC,CAAC,KAAiB;QAC3D,IAAI,KAAK,CAAC,QAAQ,IAAI,qBAAqB,CAAC,KAAK,CAAC,EAAE,CAAC;YACnD,MAAM,SAAS,GAAG,IAAI,CAAC,oBAAoB,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAC1D,IAAI,SAAS,EAAE,CAAC;gBACd,IAAI,CAAC,oBAAoB,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAC7C,CAAC;iBAAM,CAAC;gBACN,IAAI,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC;YACnD,CAAC;YACD,IAAI,CAAC,iBAAiB,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QACpC,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,sBAAsB,CAAC,CAAC,KAAK,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;CACF","sourcesContent":["import { createIncrementStateEvent, isIncrementStateEvent, type StateEvent } from './events.js';\nimport {\n $createOperation,\n $processServerResponse,\n $resolveOperation,\n $setValueQuietly,\n $update,\n type Operation,\n} from './FullStackSignal.js';\nimport { ValueSignal } from './ValueSignal.js';\n\n/**\n * A signal that holds a number value. The underlying\n * value of this signal is stored and updated as a\n * shared value on the server.\n *\n * After obtaining the NumberSignal instance from\n * a server-side service that returns one, the value\n * can be updated using the `value` property,\n * and it can be read with or without the\n * `value` property (similar to a normal signal):\n *\n * @example\n * ```tsx\n * const counter = CounterService.counter();\n *\n * return (\n * <Button onClick={() => counter.incrementBy(1)}>\n * Click count: { counter }\n * </Button>\n * <Button onClick={() => counter.value = 0}>Reset</Button>\n * );\n * ```\n */\nexport class NumberSignal extends ValueSignal<number> {\n readonly #sentIncrementEvents = new Map<string, StateEvent>();\n /**\n * Increments the value by the specified delta. The delta can be negative to\n * decrease the value.\n *\n * This method differs from using the `++` or `+=` operators directly on the\n * signal value. It performs an atomic operation to prevent conflicts from\n * concurrent changes, ensuring that other users' modifications are not\n * accidentally overwritten.\n *\n * @param delta - The delta to increment the value by. The delta can be\n * negative.\n * @returns An operation object that allows to perform additional actions.\n */\n incrementBy(delta: number): Operation {\n if (delta === 0) {\n return { result: Promise.resolve() };\n }\n this[$setValueQuietly](this.value + delta);\n const event = createIncrementStateEvent(delta);\n this.#sentIncrementEvents.set(event.id, event);\n const promise = this[$update](event);\n return this[$createOperation]({ id: event.id, promise });\n }\n\n protected override [$processServerResponse](event: StateEvent): void {\n if (event.accepted && isIncrementStateEvent(event)) {\n const sentEvent = this.#sentIncrementEvents.get(event.id);\n if (sentEvent) {\n this.#sentIncrementEvents.delete(event.id);\n } else {\n this[$setValueQuietly](this.value + event.value);\n }\n this[$resolveOperation](event.id);\n } else {\n super[$processServerResponse](event);\n }\n }\n}\n"]}
1
+ {"version":3,"file":"NumberSignal.js","sourceRoot":"","sources":["src/NumberSignal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,kBAAkB,EAAsB,MAAM,eAAe,CAAC;AAC/F,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,iBAAiB,EACjB,gBAAgB,EAChB,OAAO,GAER,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAK/C,MAAM,OAAO,YAAa,SAAQ,WAAmB;IAOnD,WAAW,CAAC,KAAa;QACvB,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC;YAChB,MAAM,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;YACnD,OAAO,IAAI,CAAC,gBAAgB,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,eAAe,EAAE,CAAC,CAAC;QACtE,CAAC;QAED,MAAM,OAAO,GAAG,sBAAsB,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;QAClD,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC;QACvC,OAAO,IAAI,CAAC,gBAAgB,CAAC,CAAC,EAAE,EAAE,EAAE,OAAO,CAAC,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC;IACpE,CAAC;IAKD,UAAU;QACR,OAAO,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC;IAEkB,CAAC,sBAAsB,CAAC,CAAC,OAAsB;QAChE,IAAI,kBAAkB,CAAC,OAAO,CAAC,EAAE,CAAC;YAChC,IAAI,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;YACnD,IAAI,CAAC,iBAAiB,CAAC,CAAC,OAAO,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;QACxD,CAAC;aAAM,CAAC;YACN,KAAK,CAAC,sBAAsB,CAAC,CAAC,OAAO,CAAC,CAAC;QACzC,CAAC;IACH,CAAC;CACF","sourcesContent":["import { createIncrementCommand, isIncrementCommand, type SignalCommand } from './commands.js';\nimport {\n $createOperation,\n $processServerResponse,\n $resolveOperation,\n $setValueQuietly,\n $update,\n type Operation,\n} from './FullStackSignal.js';\nimport { ValueSignal } from './ValueSignal.js';\n\n/**\n * A signal containing a numeric value. The value is updated as a single atomic change.\n */\nexport class NumberSignal extends ValueSignal<number> {\n /**\n * Atomically increments the value of this signal by the given delta amount.\n * The value is decremented if the delta is negative.\n * @param delta - The increment amount\n * @returns An operation containing the eventual result\n */\n incrementBy(delta: number): Operation {\n if (delta === 0) {\n const resolvedPromise = Promise.resolve(undefined);\n return this[$createOperation]({ id: '', promise: resolvedPromise });\n }\n\n const command = createIncrementCommand('', delta);\n const promise = this[$update](command);\n return this[$createOperation]({ id: command.commandId, promise });\n }\n\n /**\n * Gets the value of this signal as an integer.\n */\n valueAsInt(): number {\n return Math.trunc(this.value);\n }\n\n protected override [$processServerResponse](command: SignalCommand): void {\n if (isIncrementCommand(command)) {\n this[$setValueQuietly](this.value + command.delta);\n this[$resolveOperation](command.commandId, undefined);\n } else {\n super[$processServerResponse](command);\n }\n }\n}\n"]}
package/ValueSignal.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type StateEvent } from './events.js';
1
+ import { type SignalCommand } from './commands.js';
2
2
  import { $processServerResponse, FullStackSignal, type Operation } from './FullStackSignal.js';
3
3
  export interface OperationSubscription extends Operation {
4
4
  cancel(): void;
@@ -6,7 +6,5 @@ export interface OperationSubscription extends Operation {
6
6
  export declare class ValueSignal<T> extends FullStackSignal<T> {
7
7
  #private;
8
8
  set(value: T): Operation;
9
- replace(expected: T, newValue: T): Operation;
10
- update(callback: (value: T) => T): OperationSubscription;
11
- protected [$processServerResponse](event: StateEvent): void;
9
+ protected [$processServerResponse](command: SignalCommand): void;
12
10
  }
package/ValueSignal.js CHANGED
@@ -1,60 +1,29 @@
1
- import { nanoid } from 'nanoid';
2
- import { createReplaceStateEvent, createSetStateEvent, isReplaceStateEvent, isSetStateEvent, isSnapshotStateEvent, } from './events.js';
1
+ import { createSetCommand, isSetCommand, isSnapshotCommand } from './commands.js';
3
2
  import { $createOperation, $processServerResponse, $resolveOperation, $setValueQuietly, $update, FullStackSignal, } from './FullStackSignal.js';
4
3
  export class ValueSignal extends FullStackSignal {
5
4
  #pendingRequests = new Map();
6
5
  set(value) {
7
- const { parentClientSignalId } = this.server.config;
8
- const signalId = parentClientSignalId !== undefined ? this.id : undefined;
9
- const event = createSetStateEvent(value, signalId, parentClientSignalId);
10
- const promise = this[$update](event);
6
+ const command = createSetCommand('', value);
7
+ const promise = this[$update](command);
11
8
  this[$setValueQuietly](value);
12
- return this[$createOperation]({ id: event.id, promise });
9
+ return this[$createOperation]({ id: command.commandId, promise });
13
10
  }
14
- replace(expected, newValue) {
15
- const { parentClientSignalId } = this.server.config;
16
- const signalId = parentClientSignalId !== undefined ? this.id : undefined;
17
- const event = createReplaceStateEvent(expected, newValue, signalId, parentClientSignalId);
18
- const promise = this[$update](event);
19
- return this[$createOperation]({ id: event.id, promise });
20
- }
21
- update(callback) {
22
- const newValue = callback(this.value);
23
- const event = createReplaceStateEvent(this.value, newValue);
24
- const promise = this[$update](event);
25
- const pendingRequest = { id: nanoid(), callback, canceled: false };
26
- this.#pendingRequests.set(event.id, pendingRequest);
27
- return {
28
- ...this[$createOperation]({ id: pendingRequest.id, promise }),
29
- cancel: () => {
30
- pendingRequest.canceled = true;
31
- },
32
- };
33
- }
34
- [$processServerResponse](event) {
35
- const record = this.#pendingRequests.get(event.id);
11
+ [$processServerResponse](command) {
12
+ const record = this.#pendingRequests.get(command.commandId);
36
13
  if (record) {
37
- this.#pendingRequests.delete(event.id);
38
- if (!(event.accepted || record.canceled)) {
39
- this.update(record.callback);
40
- }
41
- }
42
- let reason;
43
- if (event.accepted || isSnapshotStateEvent(event)) {
44
- this.#applyAcceptedEvent(event);
45
- }
46
- else {
47
- reason = `Server rejected the operation with id '${event.id}'. See the server log for more details.`;
14
+ this.#pendingRequests.delete(command.commandId);
48
15
  }
49
- [record?.id, event.id].filter(Boolean).forEach((id) => this[$resolveOperation](id, reason));
16
+ this.#recalculateState(command);
17
+ [record?.id, command.commandId].filter(Boolean).forEach((id) => this[$resolveOperation](id, undefined));
50
18
  }
51
- #applyAcceptedEvent(event) {
52
- if (isSetStateEvent(event) || isSnapshotStateEvent(event)) {
53
- this.value = event.value;
19
+ #recalculateState(command) {
20
+ if (isSetCommand(command)) {
21
+ this[$setValueQuietly](command.value);
54
22
  }
55
- else if (isReplaceStateEvent(event)) {
56
- if (JSON.stringify(this.value) === JSON.stringify(event.expected)) {
57
- this.value = event.value;
23
+ else if (isSnapshotCommand(command)) {
24
+ const node = command.nodes[''];
25
+ if (node && 'value' in node) {
26
+ this[$setValueQuietly](node.value);
58
27
  }
59
28
  }
60
29
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ValueSignal.js","sourceRoot":"","sources":["src/ValueSignal.ts"],"names":[],"mappings":"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","sourcesContent":["import { nanoid } from 'nanoid';\nimport {\n createReplaceStateEvent,\n createSetStateEvent,\n isReplaceStateEvent,\n isSetStateEvent,\n isSnapshotStateEvent,\n type StateEvent,\n} from './events.js';\nimport {\n $createOperation,\n $processServerResponse,\n $resolveOperation,\n $setValueQuietly,\n $update,\n FullStackSignal,\n type Operation,\n} from './FullStackSignal.js';\n\ntype PendingRequestsRecord<T> = Readonly<{\n id: string;\n callback(value: T): T;\n}> & { canceled: boolean };\n\n/**\n * An operation subscription that can be canceled.\n */\nexport interface OperationSubscription extends Operation {\n cancel(): void;\n}\n\n/**\n * A full-stack signal that holds an arbitrary value.\n */\nexport class ValueSignal<T> extends FullStackSignal<T> {\n readonly #pendingRequests = new Map<string, PendingRequestsRecord<T>>();\n\n /**\n * Sets the value.\n * Note that the value change event that is propagated to the server as the\n * result of this operation is not taking the last seen value into account and\n * will overwrite the shared value on the server unconditionally (AKA: \"Last\n * Write Wins\"). If you need to perform a conditional update, use the\n * `replace` method instead.\n *\n * @param value - The new value.\n * @returns An operation object that allows to perform additional actions.\n */\n set(value: T): Operation {\n const { parentClientSignalId } = this.server.config;\n const signalId = parentClientSignalId !== undefined ? this.id : undefined;\n const event = createSetStateEvent(value, signalId, parentClientSignalId);\n const promise = this[$update](event);\n this[$setValueQuietly](value);\n return this[$createOperation]({ id: event.id, promise });\n }\n\n /**\n * Replaces the value with a new one only if the current value is equal to the\n * expected value.\n *\n * @param expected - The expected value.\n * @param newValue - The new value.\n * @returns An operation object that allows to perform additional actions.\n */\n replace(expected: T, newValue: T): Operation {\n const { parentClientSignalId } = this.server.config;\n const signalId = parentClientSignalId !== undefined ? this.id : undefined;\n const event = createReplaceStateEvent(expected, newValue, signalId, parentClientSignalId);\n const promise = this[$update](event);\n return this[$createOperation]({ id: event.id, promise });\n }\n\n /**\n * Tries to update the value by applying the callback function to the current\n * value. In case of a concurrent change, the callback is run again with an\n * updated input value. This is repeated until the result can be applied\n * without concurrent changes, or the operation is canceled.\n *\n * Note that there is no guarantee that cancel() will be effective always,\n * since a succeeding operation might already be on its way to the server.\n *\n * @param callback - The function that is applied on the current value to\n * produce the new value.\n * @returns An operation object that allows to perform additional actions,\n * including cancellation.\n */\n update(callback: (value: T) => T): OperationSubscription {\n const newValue = callback(this.value);\n const event = createReplaceStateEvent(this.value, newValue);\n const promise = this[$update](event);\n const pendingRequest = { id: nanoid(), callback, canceled: false };\n this.#pendingRequests.set(event.id, pendingRequest);\n return {\n ...this[$createOperation]({ id: pendingRequest.id, promise }),\n cancel: () => {\n pendingRequest.canceled = true;\n },\n };\n }\n\n protected override [$processServerResponse](event: StateEvent): void {\n const record = this.#pendingRequests.get(event.id);\n if (record) {\n this.#pendingRequests.delete(event.id);\n\n if (!(event.accepted || record.canceled)) {\n this.update(record.callback);\n }\n }\n\n let reason: string | undefined;\n if (event.accepted || isSnapshotStateEvent<T>(event)) {\n this.#applyAcceptedEvent(event);\n } else {\n reason = `Server rejected the operation with id '${event.id}'. See the server log for more details.`;\n }\n // `then` callbacks can be associated to the record or the event\n // it depends on the operation that was performed\n [record?.id, event.id].filter(Boolean).forEach((id) => this[$resolveOperation](id!, reason));\n }\n\n #applyAcceptedEvent(event: StateEvent): void {\n if (isSetStateEvent<T>(event) || isSnapshotStateEvent<T>(event)) {\n this.value = event.value;\n } else if (isReplaceStateEvent<T>(event)) {\n if (JSON.stringify(this.value) === JSON.stringify(event.expected)) {\n this.value = event.value;\n }\n }\n }\n}\n"]}
1
+ {"version":3,"file":"ValueSignal.js","sourceRoot":"","sources":["src/ValueSignal.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,iBAAiB,EAAiC,MAAM,eAAe,CAAC;AACjH,OAAO,EACL,gBAAgB,EAChB,sBAAsB,EACtB,iBAAiB,EACjB,gBAAgB,EAChB,OAAO,EACP,eAAe,GAEhB,MAAM,sBAAsB,CAAC;AAiB9B,MAAM,OAAO,WAAe,SAAQ,eAAkB;IAC3C,gBAAgB,GAAG,IAAI,GAAG,EAAoC,CAAC;IAaxE,GAAG,CAAC,KAAQ;QACV,MAAM,OAAO,GAAG,gBAAgB,CAAC,EAAE,EAAE,KAAK,CAAC,CAAC;QAC5C,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC;QACvC,IAAI,CAAC,gBAAgB,CAAC,CAAC,KAAK,CAAC,CAAC;QAC9B,OAAO,IAAI,CAAC,gBAAgB,CAAC,CAAC,EAAE,EAAE,EAAE,OAAO,CAAC,SAAS,EAAE,OAAO,EAAE,CAAC,CAAC;IACpE,CAAC;IAEkB,CAAC,sBAAsB,CAAC,CAAC,OAAsB;QAChE,MAAM,MAAM,GAAG,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAC5D,IAAI,MAAM,EAAE,CAAC;YACX,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAClD,CAAC;QAED,IAAI,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC;QAIhC,CAAC,MAAM,EAAE,EAAE,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC,EAAG,EAAE,SAAS,CAAC,CAAC,CAAC;IAC3G,CAAC;IAED,iBAAiB,CAAC,OAAsB;QACtC,IAAI,YAAY,CAAI,OAAO,CAAC,EAAE,CAAC;YAC7B,IAAI,CAAC,gBAAgB,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QACxC,CAAC;aAAM,IAAI,iBAAiB,CAAC,OAAO,CAAC,EAAE,CAAC;YACtC,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,EAAE,CAAqB,CAAC;YACnD,IAAI,IAAI,IAAI,OAAO,IAAI,IAAI,EAAE,CAAC;gBAC5B,IAAI,CAAC,gBAAgB,CAAC,CAAC,IAAI,CAAC,KAAU,CAAC,CAAC;YAC1C,CAAC;QACH,CAAC;IACH,CAAC;CACF","sourcesContent":["import { createSetCommand, isSetCommand, isSnapshotCommand, type Node, type SignalCommand } from './commands.js';\nimport {\n $createOperation,\n $processServerResponse,\n $resolveOperation,\n $setValueQuietly,\n $update,\n FullStackSignal,\n type Operation,\n} from './FullStackSignal.js';\n\ntype PendingRequestsRecord<T> = Readonly<{\n id: string;\n callback(value: T): T;\n}> & { canceled: boolean };\n\n/**\n * An operation subscription that can be canceled.\n */\nexport interface OperationSubscription extends Operation {\n cancel(): void;\n}\n\n/**\n * A full-stack signal that holds an arbitrary value.\n */\nexport class ValueSignal<T> extends FullStackSignal<T> {\n readonly #pendingRequests = new Map<string, PendingRequestsRecord<T>>();\n\n /**\n * Sets the value.\n * Note that the value change event that is propagated to the server as the\n * result of this operation is not taking the last seen value into account and\n * will overwrite the shared value on the server unconditionally (AKA: \"Last\n * Write Wins\"). If you need to perform a conditional update, use the\n * `replace` method instead.\n *\n * @param value - The new value.\n * @returns An operation object that allows to perform additional actions.\n */\n set(value: T): Operation {\n const command = createSetCommand('', value);\n const promise = this[$update](command);\n this[$setValueQuietly](value);\n return this[$createOperation]({ id: command.commandId, promise });\n }\n\n protected override [$processServerResponse](command: SignalCommand): void {\n const record = this.#pendingRequests.get(command.commandId);\n if (record) {\n this.#pendingRequests.delete(command.commandId);\n }\n\n this.#recalculateState(command);\n\n // `then` callbacks can be associated to the record or the event\n // it depends on the operation that was performed\n [record?.id, command.commandId].filter(Boolean).forEach((id) => this[$resolveOperation](id!, undefined));\n }\n\n #recalculateState(command: SignalCommand): void {\n if (isSetCommand<T>(command)) {\n this[$setValueQuietly](command.value);\n } else if (isSnapshotCommand(command)) {\n const node = command.nodes[''] as Node | undefined;\n if (node && 'value' in node) {\n this[$setValueQuietly](node.value as T);\n }\n }\n }\n}\n"]}
package/commands.d.ts ADDED
@@ -0,0 +1,88 @@
1
+ export type Id = string;
2
+ export type SignalCommand = Readonly<{
3
+ commandId: Id;
4
+ targetNodeId: Id;
5
+ '@type': string;
6
+ }>;
7
+ type CreateCommandType<T extends string, E extends Record<string, unknown> = Record<never, never>> = Readonly<{
8
+ '@type': T;
9
+ }> & Readonly<E> & SignalCommand;
10
+ export type ValueCondition<V> = CreateCommandType<'value', {
11
+ expectedValue: V;
12
+ }>;
13
+ export declare function createValueCondition<V>(targetNodeId: Id, expectedValue: V): ValueCondition<V>;
14
+ export type SetCommand<V> = CreateCommandType<'set', {
15
+ value: V;
16
+ }>;
17
+ export declare function createSetCommand<V>(targetNodeId: Id, value: V): SetCommand<V>;
18
+ export type IncrementCommand = CreateCommandType<'inc', {
19
+ delta: number;
20
+ }>;
21
+ export declare function createIncrementCommand(targetNodeId: Id, delta: number): IncrementCommand;
22
+ export type TransactionCommand = CreateCommandType<'tx', {
23
+ commands: SignalCommand[];
24
+ }>;
25
+ export declare function createTransactionCommand(commands: SignalCommand[]): TransactionCommand;
26
+ export type InsertCommand<V> = CreateCommandType<'insert', {
27
+ value: V;
28
+ position: ListPosition;
29
+ }>;
30
+ export declare const ZERO: Id;
31
+ export declare function createInsertCommand<V>(targetNodeId: Id, value: V, position: ListPosition): InsertCommand<V>;
32
+ export type ListPosition = {
33
+ after?: Id | null;
34
+ before?: Id | null;
35
+ };
36
+ export declare const EDGE: Id;
37
+ export declare const ListPosition: {
38
+ first(): ListPosition;
39
+ last(): ListPosition;
40
+ after(signal: {
41
+ id: Id;
42
+ } | null): ListPosition;
43
+ before(signal: {
44
+ id: Id;
45
+ } | null): ListPosition;
46
+ between(after: {
47
+ id: Id;
48
+ } | null, before: {
49
+ id: Id;
50
+ } | null): ListPosition;
51
+ };
52
+ export type AdoptAtCommand = CreateCommandType<'at', {
53
+ childId: Id;
54
+ position: ListPosition;
55
+ }>;
56
+ export declare function createAdoptAtCommand(targetNodeId: Id, childId: Id, position: ListPosition): AdoptAtCommand;
57
+ export type PositionCondition = CreateCommandType<'pos', {
58
+ childId: Id;
59
+ expectedPosition: ListPosition;
60
+ }>;
61
+ export declare function createPositionCondition(targetNodeId: Id, childId: Id, expectedPosition: ListPosition): PositionCondition;
62
+ export type RemoveCommand = CreateCommandType<'remove', {
63
+ expectedParentId: Id;
64
+ }>;
65
+ export declare function createRemoveCommand(targetNodeId: Id, expectedParentId: Id): RemoveCommand;
66
+ export type Node = {
67
+ '@type': string;
68
+ parent: Id | null;
69
+ lastUpdate: Id | null;
70
+ scopeOwner: Id | null;
71
+ value?: unknown;
72
+ listChildren: Id[];
73
+ mapChildren: Record<string, Id>;
74
+ };
75
+ export type SnapshotCommand = CreateCommandType<'snapshot', {
76
+ nodes: Record<Id, Node>;
77
+ }>;
78
+ export declare function createSnapshotCommand(nodes: Record<Id, Node>): SnapshotCommand;
79
+ export declare function isSetCommand<V>(command: unknown): command is SetCommand<V>;
80
+ export declare function isValueCondition<V>(command: unknown): command is ValueCondition<V>;
81
+ export declare function isIncrementCommand(command: unknown): command is IncrementCommand;
82
+ export declare function isTransactionCommand(command: unknown): command is TransactionCommand;
83
+ export declare function isInsertCommand<V>(command: unknown): command is InsertCommand<V>;
84
+ export declare function isAdoptAtCommand(command: unknown): command is AdoptAtCommand;
85
+ export declare function isPositionCondition(command: unknown): command is PositionCondition;
86
+ export declare function isRemoveCommand(command: unknown): command is RemoveCommand;
87
+ export declare function isSnapshotCommand(command: unknown): command is SnapshotCommand;
88
+ export {};
package/commands.js ADDED
@@ -0,0 +1,135 @@
1
+ import { randomId } from './utils';
2
+ export function createValueCondition(targetNodeId, expectedValue) {
3
+ return {
4
+ commandId: randomId(),
5
+ targetNodeId,
6
+ '@type': 'value',
7
+ expectedValue,
8
+ };
9
+ }
10
+ export function createSetCommand(targetNodeId, value) {
11
+ return {
12
+ commandId: randomId(),
13
+ targetNodeId,
14
+ '@type': 'set',
15
+ value,
16
+ };
17
+ }
18
+ export function createIncrementCommand(targetNodeId, delta) {
19
+ return {
20
+ commandId: randomId(),
21
+ targetNodeId,
22
+ '@type': 'inc',
23
+ delta,
24
+ };
25
+ }
26
+ export function createTransactionCommand(commands) {
27
+ return {
28
+ commandId: randomId(),
29
+ targetNodeId: '',
30
+ '@type': 'tx',
31
+ commands,
32
+ };
33
+ }
34
+ export const ZERO = '';
35
+ export function createInsertCommand(targetNodeId, value, position) {
36
+ return {
37
+ commandId: randomId(),
38
+ targetNodeId,
39
+ '@type': 'insert',
40
+ value,
41
+ position,
42
+ };
43
+ }
44
+ export const EDGE = '';
45
+ function idOf(signal) {
46
+ return signal?.id ?? EDGE;
47
+ }
48
+ export const ListPosition = {
49
+ first() {
50
+ return { after: EDGE, before: null };
51
+ },
52
+ last() {
53
+ return { after: null, before: EDGE };
54
+ },
55
+ after(signal) {
56
+ return { after: idOf(signal), before: null };
57
+ },
58
+ before(signal) {
59
+ return { after: null, before: idOf(signal) };
60
+ },
61
+ between(after, before) {
62
+ return { after: idOf(after), before: idOf(before) };
63
+ },
64
+ };
65
+ export function createAdoptAtCommand(targetNodeId, childId, position) {
66
+ return {
67
+ commandId: randomId(),
68
+ targetNodeId,
69
+ '@type': 'at',
70
+ childId,
71
+ position,
72
+ };
73
+ }
74
+ export function createPositionCondition(targetNodeId, childId, expectedPosition) {
75
+ return {
76
+ commandId: randomId(),
77
+ targetNodeId,
78
+ '@type': 'pos',
79
+ childId,
80
+ expectedPosition,
81
+ };
82
+ }
83
+ export function createRemoveCommand(targetNodeId, expectedParentId) {
84
+ return {
85
+ commandId: randomId(),
86
+ targetNodeId,
87
+ '@type': 'remove',
88
+ expectedParentId,
89
+ };
90
+ }
91
+ export function createSnapshotCommand(nodes) {
92
+ return {
93
+ commandId: randomId(),
94
+ targetNodeId: '',
95
+ '@type': 'snapshot',
96
+ nodes,
97
+ };
98
+ }
99
+ function isSignalCommand(command) {
100
+ return (typeof command === 'object' &&
101
+ command !== null &&
102
+ typeof command.commandId === 'string' &&
103
+ typeof command['@type'] === 'string');
104
+ }
105
+ export function isSetCommand(command) {
106
+ return isSignalCommand(command) && command['@type'] === 'set';
107
+ }
108
+ export function isValueCondition(command) {
109
+ return isSignalCommand(command) && command['@type'] === 'value';
110
+ }
111
+ export function isIncrementCommand(command) {
112
+ return isSignalCommand(command) && command['@type'] === 'inc';
113
+ }
114
+ export function isTransactionCommand(command) {
115
+ return (isSignalCommand(command) &&
116
+ command['@type'] === 'tx' &&
117
+ command.targetNodeId === '' &&
118
+ Array.isArray(command.commands));
119
+ }
120
+ export function isInsertCommand(command) {
121
+ return isSignalCommand(command) && command['@type'] === 'insert';
122
+ }
123
+ export function isAdoptAtCommand(command) {
124
+ return isSignalCommand(command) && command['@type'] === 'at';
125
+ }
126
+ export function isPositionCondition(command) {
127
+ return isSignalCommand(command) && command['@type'] === 'pos';
128
+ }
129
+ export function isRemoveCommand(command) {
130
+ return isSignalCommand(command) && command['@type'] === 'remove';
131
+ }
132
+ export function isSnapshotCommand(command) {
133
+ return isSignalCommand(command) && command['@type'] === 'snapshot';
134
+ }
135
+ //# sourceMappingURL=commands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.js","sourceRoot":"","sources":["src/commands.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomId } from './utils';\n\n/**\n * A base64 string value emitted by Jackson's JsonValue annotated properties.\n */\nexport type Id = string;\n\n/**\n * A command triggered from a signal.\n */\nexport type SignalCommand = Readonly<{\n commandId: Id;\n targetNodeId: Id;\n '@type': string;\n}>;\n\n/**\n * Creates a new state event type.\n */\ntype CreateCommandType<T extends string, E extends Record<string, unknown> = Record<never, never>> = Readonly<{\n '@type': T;\n}> &\n Readonly<E> &\n SignalCommand;\n\n/**\n * A signal command that doesn't apply any change but only performs a test\n * whether the given node has the expected value, based on JSON equality.\n */\nexport type ValueCondition<V> = CreateCommandType<'value', { expectedValue: V }>;\n\nexport function createValueCondition<V>(targetNodeId: Id, expectedValue: V): ValueCondition<V> {\n return {\n commandId: randomId(),\n targetNodeId,\n '@type': 'value',\n expectedValue,\n };\n}\n\n/**\n * A signal command that sets a value.\n */\nexport type SetCommand<V> = CreateCommandType<'set', { value: V }>;\n\nexport function createSetCommand<V>(targetNodeId: Id, value: V): SetCommand<V> {\n return {\n commandId: randomId(),\n targetNodeId,\n '@type': 'set',\n value,\n };\n}\n\n/**\n * A signal command that increments a numeric value.\n */\nexport type IncrementCommand = CreateCommandType<'inc', { delta: number }>;\n\nexport function createIncrementCommand(targetNodeId: Id, delta: number): IncrementCommand {\n return {\n commandId: randomId(),\n targetNodeId,\n '@type': 'inc',\n delta,\n };\n}\n\n/**\n * A sequence of commands that should be applied atomically and only if all\n * commands are individually accepted.\n */\nexport type TransactionCommand = CreateCommandType<\n 'tx',\n {\n commands: SignalCommand[];\n }\n>;\n\nexport function createTransactionCommand(commands: SignalCommand[]): TransactionCommand {\n return {\n commandId: randomId(),\n targetNodeId: '',\n '@type': 'tx',\n commands,\n };\n}\n\n/**\n * A signal command that inserts a value into a list at a given position.\n */\nexport type InsertCommand<V> = CreateCommandType<\n 'insert',\n {\n value: V;\n position: ListPosition;\n }\n>;\n\n// ZERO constant to represent the default id (like Id.ZERO in Java)\nexport const ZERO: Id = '';\n\nexport function createInsertCommand<V>(targetNodeId: Id, value: V, position: ListPosition): InsertCommand<V> {\n return {\n commandId: randomId(),\n targetNodeId,\n '@type': 'insert',\n value,\n position,\n };\n}\n\nexport type ListPosition = {\n after?: Id | null;\n before?: Id | null;\n};\n\n// EDGE constant to represent the edge of the list (like Id.EDGE in Java)\nexport const EDGE: Id = '';\n\nfunction idOf(signal: { id: Id } | null | undefined): Id {\n return signal?.id ?? EDGE;\n}\n\n// ListPosition helpers to match Java API.\nexport const ListPosition = {\n /**\n * Gets the insertion position that corresponds to the beginning of the list.\n * After edge.\n */\n first(): ListPosition {\n return { after: EDGE, before: null };\n },\n /**\n * Gets the insertion position that corresponds to the end of the list.\n * Before edge.\n */\n last(): ListPosition {\n return { after: null, before: EDGE };\n },\n /**\n * Gets the insertion position immediately after the given signal.\n * Inserting after null is interpreted as after the start of the list (first).\n */\n after(signal: { id: Id } | null): ListPosition {\n return { after: idOf(signal), before: null };\n },\n /**\n * Gets the insertion position immediately before the given signal.\n * Inserting before null is interpreted as before the end of the list (last).\n */\n before(signal: { id: Id } | null): ListPosition {\n return { after: null, before: idOf(signal) };\n },\n /**\n * Gets the insertion position between the given signals.\n * Inserting after null is after the start (first), before null is before the end (last).\n */\n between(after: { id: Id } | null, before: { id: Id } | null): ListPosition {\n return { after: idOf(after), before: idOf(before) };\n },\n};\n\n/**\n * A signal command that moves a child to a new position in a list.\n */\nexport type AdoptAtCommand = CreateCommandType<\n 'at',\n {\n childId: Id;\n position: ListPosition;\n }\n>;\n\nexport function createAdoptAtCommand(targetNodeId: Id, childId: Id, position: ListPosition): AdoptAtCommand {\n return {\n commandId: randomId(),\n targetNodeId,\n '@type': 'at',\n childId,\n position,\n };\n}\n\nexport type PositionCondition = CreateCommandType<\n 'pos',\n {\n childId: Id;\n expectedPosition: ListPosition;\n }\n>;\n\nexport function createPositionCondition(\n targetNodeId: Id,\n childId: Id,\n expectedPosition: ListPosition,\n): PositionCondition {\n return {\n commandId: randomId(),\n targetNodeId,\n '@type': 'pos',\n childId,\n expectedPosition,\n };\n}\n\nexport type RemoveCommand = CreateCommandType<\n 'remove',\n {\n expectedParentId: Id;\n }\n>;\n\nexport function createRemoveCommand(targetNodeId: Id, expectedParentId: Id): RemoveCommand {\n return {\n commandId: randomId(),\n targetNodeId,\n '@type': 'remove',\n expectedParentId,\n };\n}\n\n/**\n * A node in the signal tree, as used in SnapshotCommand.\n */\nexport type Node = {\n '@type': string;\n parent: Id | null;\n lastUpdate: Id | null;\n scopeOwner: Id | null;\n value?: unknown;\n listChildren: Id[];\n mapChildren: Record<string, Id>;\n};\n\n/**\n * A signal command that initializes a tree based on a collection of pre-existing nodes.\n */\nexport type SnapshotCommand = CreateCommandType<\n 'snapshot',\n {\n nodes: Record<Id, Node>;\n }\n>;\n\nexport function createSnapshotCommand(nodes: Record<Id, Node>): SnapshotCommand {\n return {\n commandId: randomId(),\n targetNodeId: '',\n '@type': 'snapshot',\n nodes,\n };\n}\n\n// TypeGuard functions:\n\nfunction isSignalCommand(command: unknown): command is SignalCommand {\n return (\n typeof command === 'object' &&\n command !== null &&\n typeof (command as { commandId?: unknown }).commandId === 'string' &&\n typeof (command as { ['@type']?: unknown })['@type'] === 'string'\n );\n}\n\nexport function isSetCommand<V>(command: unknown): command is SetCommand<V> {\n return isSignalCommand(command) && command['@type'] === 'set';\n}\n\nexport function isValueCondition<V>(command: unknown): command is ValueCondition<V> {\n return isSignalCommand(command) && command['@type'] === 'value';\n}\n\nexport function isIncrementCommand(command: unknown): command is IncrementCommand {\n return isSignalCommand(command) && command['@type'] === 'inc';\n}\n\nexport function isTransactionCommand(command: unknown): command is TransactionCommand {\n return (\n isSignalCommand(command) &&\n command['@type'] === 'tx' &&\n command.targetNodeId === '' &&\n Array.isArray((command as TransactionCommand).commands)\n );\n}\n\nexport function isInsertCommand<V>(command: unknown): command is InsertCommand<V> {\n return isSignalCommand(command) && command['@type'] === 'insert';\n}\n\nexport function isAdoptAtCommand(command: unknown): command is AdoptAtCommand {\n return isSignalCommand(command) && command['@type'] === 'at';\n}\n\nexport function isPositionCondition(command: unknown): command is PositionCondition {\n return isSignalCommand(command) && command['@type'] === 'pos';\n}\n\nexport function isRemoveCommand(command: unknown): command is RemoveCommand {\n return isSignalCommand(command) && command['@type'] === 'remove';\n}\n\nexport function isSnapshotCommand(command: unknown): command is SnapshotCommand {\n return isSignalCommand(command) && command['@type'] === 'snapshot';\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vaadin/hilla-react-signals",
3
- "version": "24.9.2",
3
+ "version": "25.0.0-alpha10",
4
4
  "description": "Signals for Hilla React",
5
5
  "main": "index.js",
6
6
  "module": "index.js",
@@ -45,7 +45,7 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@preact/signals-react": "3.0.1",
48
- "@vaadin/hilla-frontend": "24.9.2",
48
+ "@vaadin/hilla-frontend": "25.0.0-alpha10",
49
49
  "nanoid": "5.0.9"
50
50
  },
51
51
  "peerDependencies": {
package/utils.d.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  import type { ReadonlySignal } from '@preact/signals-react';
2
2
  export declare function createPromiseFromSignal<T, U, E>(signal: ReadonlySignal<T>, callback: (value: T, resolve: (value: PromiseLike<U> | U) => void, reject: (reason?: E) => void) => void): Promise<U>;
3
+ export declare function randomId(sizeBytes?: number): string;
package/utils.js CHANGED
@@ -9,4 +9,18 @@ export async function createPromiseFromSignal(signal, callback) {
9
9
  });
10
10
  });
11
11
  }
12
+ export function randomId(sizeBytes = 8) {
13
+ const bytes = new Uint8Array(sizeBytes);
14
+ crypto.getRandomValues(bytes);
15
+ let binary = '';
16
+ for (const value of bytes) {
17
+ binary += String.fromCharCode(value);
18
+ }
19
+ const base64 = btoa(binary);
20
+ let end = base64.length;
21
+ while (end > 0 && base64[end - 1] === '=') {
22
+ end -= 1;
23
+ }
24
+ return base64.slice(0, end);
25
+ }
12
26
  //# sourceMappingURL=utils.js.map
package/utils.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"utils.js","sourceRoot":"","sources":["src/utils.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,MAAyB,EACzB,QAAwG;IAExG,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,WAAW,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;YAC7C,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,OAAO;YACT,CAAC;YAED,WAAW,EAAE,CAAC;YACd,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC","sourcesContent":["import type { ReadonlySignal } from '@preact/signals-react';\n\nexport async function createPromiseFromSignal<T, U, E>(\n signal: ReadonlySignal<T>,\n callback: (value: T, resolve: (value: PromiseLike<U> | U) => void, reject: (reason?: E) => void) => void,\n): Promise<U> {\n return new Promise((resolve, reject) => {\n const unsubscribe = signal.subscribe((value) => {\n if (!value) {\n return;\n }\n\n unsubscribe();\n callback(value, resolve, reject);\n });\n });\n}\n"]}
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["src/utils.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAC3C,MAAyB,EACzB,QAAwG;IAExG,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,WAAW,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;YAC7C,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,OAAO;YACT,CAAC;YAED,WAAW,EAAE,CAAC;YACd,QAAQ,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC;AAOD,MAAM,UAAU,QAAQ,CAAC,SAAS,GAAG,CAAC;IACpC,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,SAAS,CAAC,CAAC;IACxC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;IAC9B,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;QAC1B,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;IACvC,CAAC;IACD,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5B,IAAI,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC;IACxB,OAAO,GAAG,GAAG,CAAC,IAAI,MAAM,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,GAAG,EAAE,CAAC;QAC1C,GAAG,IAAI,CAAC,CAAC;IACX,CAAC;IACD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AAC9B,CAAC","sourcesContent":["import type { ReadonlySignal } from '@preact/signals-react';\n\nexport async function createPromiseFromSignal<T, U, E>(\n signal: ReadonlySignal<T>,\n callback: (value: T, resolve: (value: PromiseLike<U> | U) => void, reject: (reason?: E) => void) => void,\n): Promise<U> {\n return new Promise((resolve, reject) => {\n const unsubscribe = signal.subscribe((value) => {\n if (!value) {\n return;\n }\n\n unsubscribe();\n callback(value, resolve, reject);\n });\n });\n}\n\n/**\n * Generates a random base64-encoded string of the specified size in bytes.\n * @param sizeBytes - The size of the random string in bytes. Default is 8 bytes.\n * @returns A base64-encoded string.\n */\nexport function randomId(sizeBytes = 8): string {\n const bytes = new Uint8Array(sizeBytes);\n crypto.getRandomValues(bytes);\n let binary = '';\n for (const value of bytes) {\n binary += String.fromCharCode(value);\n }\n const base64 = btoa(binary);\n let end = base64.length;\n while (end > 0 && base64[end - 1] === '=') {\n end -= 1;\n }\n return base64.slice(0, end);\n}\n"]}
package/events.d.ts DELETED
@@ -1,46 +0,0 @@
1
- export type StateEvent = Readonly<{
2
- id: string;
3
- type: string;
4
- value: unknown;
5
- accepted: boolean;
6
- parentSignalId?: string;
7
- }>;
8
- type CreateStateEventType<V, T extends string, C extends Record<string, unknown> = Record<never, never>> = Readonly<{
9
- type: T;
10
- value: V;
11
- }> & Readonly<C> & StateEvent;
12
- export type SnapshotStateEvent<T> = CreateStateEventType<T, 'snapshot'>;
13
- export type SetStateEvent<T> = CreateStateEventType<T, 'set'>;
14
- export declare function createSetStateEvent<T>(value: T, signalId?: string, parentSignalId?: string): SetStateEvent<T>;
15
- export type ReplaceStateEvent<T> = CreateStateEventType<T, 'replace', {
16
- expected: T;
17
- }>;
18
- export declare function createReplaceStateEvent<T>(expected: T, value: T, signalId?: string, parentSignalId?: string): ReplaceStateEvent<T>;
19
- export type IncrementStateEvent = CreateStateEventType<number, 'increment'>;
20
- export declare function createIncrementStateEvent(delta: number): IncrementStateEvent;
21
- export type ListEntry<T> = Readonly<{
22
- id: string;
23
- prev?: string;
24
- next?: string;
25
- value: T;
26
- }>;
27
- export type ListSnapshotStateEvent<T> = CreateStateEventType<never, 'snapshot', {
28
- entries: Array<ListEntry<T>>;
29
- }>;
30
- export type InsertLastStateEvent<T> = CreateStateEventType<T, 'insert', {
31
- position: 'last';
32
- entryId?: string;
33
- }>;
34
- export declare function createInsertLastStateEvent<T>(value: T): InsertLastStateEvent<T>;
35
- export type RemoveStateEvent = CreateStateEventType<never, 'remove', {
36
- entryId: string;
37
- }>;
38
- export declare function createRemoveStateEvent(entryId: string): RemoveStateEvent;
39
- export declare function isSnapshotStateEvent<T>(event: unknown): event is SnapshotStateEvent<T>;
40
- export declare function isSetStateEvent<T>(event: unknown): event is SetStateEvent<T>;
41
- export declare function isReplaceStateEvent<T>(event: unknown): event is ReplaceStateEvent<T>;
42
- export declare function isIncrementStateEvent(event: unknown): event is IncrementStateEvent;
43
- export declare function isListSnapshotStateEvent<T>(event: unknown): event is ListSnapshotStateEvent<T>;
44
- export declare function isInsertLastStateEvent<T>(event: unknown): event is InsertLastStateEvent<T>;
45
- export declare function isRemoveStateEvent(event: unknown): event is RemoveStateEvent;
46
- export {};
package/events.js DELETED
@@ -1,86 +0,0 @@
1
- import { nanoid } from 'nanoid';
2
- export function createSetStateEvent(value, signalId, parentSignalId) {
3
- return {
4
- id: signalId ?? nanoid(),
5
- type: 'set',
6
- value,
7
- accepted: false,
8
- ...(parentSignalId !== undefined ? { parentSignalId } : {}),
9
- };
10
- }
11
- export function createReplaceStateEvent(expected, value, signalId, parentSignalId) {
12
- return {
13
- id: signalId ?? nanoid(),
14
- type: 'replace',
15
- value,
16
- expected,
17
- accepted: false,
18
- ...(parentSignalId !== undefined ? { parentSignalId } : {}),
19
- };
20
- }
21
- export function createIncrementStateEvent(delta) {
22
- return {
23
- id: nanoid(),
24
- type: 'increment',
25
- value: delta,
26
- accepted: false,
27
- };
28
- }
29
- export function createInsertLastStateEvent(value) {
30
- return {
31
- id: nanoid(),
32
- type: 'insert',
33
- value,
34
- position: 'last',
35
- accepted: false,
36
- };
37
- }
38
- export function createRemoveStateEvent(entryId) {
39
- return {
40
- id: nanoid(),
41
- type: 'remove',
42
- entryId,
43
- value: undefined,
44
- accepted: false,
45
- };
46
- }
47
- function isStateEvent(event) {
48
- return (typeof event === 'object' &&
49
- event !== null &&
50
- typeof event.id === 'string' &&
51
- typeof event.type === 'string' &&
52
- typeof event.value !== 'undefined' &&
53
- typeof event.accepted === 'boolean');
54
- }
55
- export function isSnapshotStateEvent(event) {
56
- return isStateEvent(event) && event.type === 'snapshot';
57
- }
58
- export function isSetStateEvent(event) {
59
- return isStateEvent(event) && event.type === 'set';
60
- }
61
- export function isReplaceStateEvent(event) {
62
- return (isStateEvent(event) && typeof event.expected !== 'undefined' && event.type === 'replace');
63
- }
64
- export function isIncrementStateEvent(event) {
65
- return isStateEvent(event) && event.type === 'increment';
66
- }
67
- export function isListSnapshotStateEvent(event) {
68
- return (typeof event === 'object' &&
69
- event !== null &&
70
- typeof event.id === 'string' &&
71
- event.type === 'snapshot' &&
72
- event.entries instanceof Array &&
73
- typeof event.accepted !== 'undefined');
74
- }
75
- export function isInsertLastStateEvent(event) {
76
- return isStateEvent(event) && event.type === 'insert' && event.position === 'last';
77
- }
78
- export function isRemoveStateEvent(event) {
79
- return (typeof event === 'object' &&
80
- event !== null &&
81
- typeof event.id === 'string' &&
82
- event.type === 'remove' &&
83
- typeof event.entryId === 'string' &&
84
- typeof event.value === 'undefined');
85
- }
86
- //# sourceMappingURL=events.js.map
package/events.js.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"events.js","sourceRoot":"","sources":["src/events.ts"],"names":[],"mappings":"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","sourcesContent":["import { nanoid } from 'nanoid';\n\nexport type StateEvent = Readonly<{\n id: string;\n type: string;\n value: unknown;\n accepted: boolean;\n parentSignalId?: string;\n}>;\n\n/**\n * Creates a new state event type.\n */\ntype CreateStateEventType<V, T extends string, C extends Record<string, unknown> = Record<never, never>> = Readonly<{\n type: T;\n value: V;\n}> &\n Readonly<C> &\n StateEvent;\n\n/**\n * A state event received from the server describing the current state of the\n * signal.\n */\nexport type SnapshotStateEvent<T> = CreateStateEventType<T, 'snapshot'>;\n\n/**\n * A state event defines a new value of the signal shared with the server. The\n */\nexport type SetStateEvent<T> = CreateStateEventType<T, 'set'>;\n\nexport function createSetStateEvent<T>(value: T, signalId?: string, parentSignalId?: string): SetStateEvent<T> {\n return {\n id: signalId ?? nanoid(),\n type: 'set',\n value,\n accepted: false,\n ...(parentSignalId !== undefined ? { parentSignalId } : {}),\n };\n}\n\nexport type ReplaceStateEvent<T> = CreateStateEventType<T, 'replace', { expected: T }>;\n\nexport function createReplaceStateEvent<T>(\n expected: T,\n value: T,\n signalId?: string,\n parentSignalId?: string,\n): ReplaceStateEvent<T> {\n return {\n id: signalId ?? nanoid(),\n type: 'replace',\n value,\n expected,\n accepted: false,\n ...(parentSignalId !== undefined ? { parentSignalId } : {}),\n };\n}\n\nexport type IncrementStateEvent = CreateStateEventType<number, 'increment'>;\n\nexport function createIncrementStateEvent(delta: number): IncrementStateEvent {\n return {\n id: nanoid(),\n type: 'increment',\n value: delta,\n accepted: false,\n };\n}\n\nexport type ListEntry<T> = Readonly<{\n id: string;\n prev?: string;\n next?: string;\n value: T;\n}>;\n\nexport type ListSnapshotStateEvent<T> = CreateStateEventType<never, 'snapshot', { entries: Array<ListEntry<T>> }>;\n\nexport type InsertLastStateEvent<T> = CreateStateEventType<T, 'insert', { position: 'last'; entryId?: string }>;\n\nexport function createInsertLastStateEvent<T>(value: T): InsertLastStateEvent<T> {\n return {\n id: nanoid(),\n type: 'insert',\n value,\n position: 'last',\n accepted: false,\n };\n}\n\nexport type RemoveStateEvent = CreateStateEventType<never, 'remove', { entryId: string }>;\n\nexport function createRemoveStateEvent(entryId: string): RemoveStateEvent {\n return {\n id: nanoid(),\n type: 'remove',\n entryId,\n value: undefined as never,\n accepted: false,\n };\n}\n\nfunction isStateEvent(event: unknown): event is StateEvent {\n return (\n typeof event === 'object' &&\n event !== null &&\n typeof (event as { id?: unknown }).id === 'string' &&\n typeof (event as { type?: unknown }).type === 'string' &&\n typeof (event as { value?: unknown }).value !== 'undefined' &&\n typeof (event as { accepted?: unknown }).accepted === 'boolean'\n );\n}\n\nexport function isSnapshotStateEvent<T>(event: unknown): event is SnapshotStateEvent<T> {\n return isStateEvent(event) && event.type === 'snapshot';\n}\n\nexport function isSetStateEvent<T>(event: unknown): event is SetStateEvent<T> {\n return isStateEvent(event) && event.type === 'set';\n}\n\nexport function isReplaceStateEvent<T>(event: unknown): event is ReplaceStateEvent<T> {\n return (\n isStateEvent(event) && typeof (event as { expected?: unknown }).expected !== 'undefined' && event.type === 'replace'\n );\n}\n\nexport function isIncrementStateEvent(event: unknown): event is IncrementStateEvent {\n return isStateEvent(event) && event.type === 'increment';\n}\n\nexport function isListSnapshotStateEvent<T>(event: unknown): event is ListSnapshotStateEvent<T> {\n return (\n typeof event === 'object' &&\n event !== null &&\n typeof (event as { id?: unknown }).id === 'string' &&\n (event as { type?: unknown }).type === 'snapshot' &&\n (event as { entries?: unknown }).entries instanceof Array &&\n typeof (event as { accepted?: unknown }).accepted !== 'undefined'\n );\n}\n\nexport function isInsertLastStateEvent<T>(event: unknown): event is InsertLastStateEvent<T> {\n return isStateEvent(event) && event.type === 'insert' && (event as { position?: unknown }).position === 'last';\n}\n\nexport function isRemoveStateEvent(event: unknown): event is RemoveStateEvent {\n return (\n typeof event === 'object' &&\n event !== null &&\n typeof (event as { id?: unknown }).id === 'string' &&\n (event as { type?: unknown }).type === 'remove' &&\n typeof (event as { entryId?: unknown }).entryId === 'string' &&\n typeof (event as { value?: unknown }).value === 'undefined'\n );\n}\n"]}