@oscarpalmer/mora 0.30.0 → 0.31.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  import {select} from '@oscarpalmer/atoms/array';
2
2
  import {filter} from '@oscarpalmer/atoms/array/filter';
3
3
  import {noop} from '@oscarpalmer/atoms/function';
4
+ import {isPlainObject} from '@oscarpalmer/atoms/is';
4
5
  import type {GenericCallback} from '@oscarpalmer/atoms/models';
5
6
  import {
6
7
  METHODS_AFFECTING_LENGTH,
@@ -22,11 +23,13 @@ import type {
22
23
  ReactiveArray,
23
24
  ReactiveOptions,
24
25
  ReactiveState,
26
+ ReadonlyInstances,
25
27
  Signal,
26
28
  } from '../models';
27
29
  import {subscribe, unsubscribe} from '../subscription';
28
30
  import {computed} from './computed';
29
31
  import {reactive} from './reactive';
32
+ import {getReadonlyInstance} from './readonly';
30
33
  import {signal} from './signal';
31
34
 
32
35
  /**
@@ -70,6 +73,9 @@ export function array<Item>(
70
73
 
71
74
  const indiced = new Map<number, [Computed<unknown>, ComputedEffect]>();
72
75
  const length = signal(0);
76
+ const readonlies: ReadonlyInstances<Item[]> = {};
77
+
78
+ let instance: Record<string, unknown> = {};
73
79
 
74
80
  state.value = new Proxy([], {
75
81
  get: (target: Item[], property: PropertyKey) =>
@@ -96,43 +102,23 @@ export function array<Item>(
96
102
  return getArrayValue(instance as ReactiveArray<Item>, indiced, state, length, value);
97
103
  }
98
104
 
99
- const instance = {
105
+ function set(first?: unknown, second?: unknown): void {
106
+ setProxyValue<Item[], Item>(
107
+ true,
108
+ state,
109
+ isArrayValue,
110
+ isArrayIndex,
111
+ setArray,
112
+ setAtIndex,
113
+ first,
114
+ second,
115
+ );
116
+ }
117
+
118
+ const handlers = {
100
119
  ...rx,
101
- length: state.value.length,
102
- at: (index: number): Item | undefined => get(index),
103
- clear: () => {
104
- state.value.length = 0;
105
- },
106
- filter: (callback: (item: Item, index: number, array: Item[]) => boolean) =>
107
- computed(() => filter(get(), callback)),
108
120
  get: (value?: number | typeof PROPERTY_LENGTH) => get(value),
109
- map: <Mapped>(callback: (item: Item, index: number, array: Item[]) => Mapped) =>
110
- computed(() => (get() as Item[]).map(callback)),
111
- notify: () => {
112
- emityProxyValues(state, indiced);
113
- },
114
- peek: (value?: unknown) => peekArrayValue(state, length, value),
115
- pop: () => state.value.pop(),
116
- push: (...items: Item[]) => state.value.push(...items),
117
- select: <Mapped>(
118
- filter: (item: Item, index: number, array: Item[]) => boolean,
119
- map: (item: Item, index: number, array: Item[]) => Mapped,
120
- ) => computed(() => select(get() as Item[], filter, map)),
121
- set: (first?: unknown, second?: unknown) => {
122
- setProxyValue<Item[], Item>(
123
- true,
124
- state,
125
- isArrayValue,
126
- isArrayIndex,
127
- setArray,
128
- setAtIndex,
129
- first,
130
- second,
131
- );
132
- },
133
- shift: () => state.value.shift(),
134
- splice: (from: number, to?: number, ...items: Item[]) =>
135
- state.value.splice(from, to ?? state.value.length, ...items),
121
+ peek: (first?: unknown, second?: boolean) => peekArrayValue(state, length, first, second),
136
122
  subscribe: (first: number | GenericCallback, second?: GenericCallback) => {
137
123
  if (typeof first === 'number' && typeof second === 'function') {
138
124
  return getReactiveValueInProxy(
@@ -145,7 +131,6 @@ export function array<Item>(
145
131
 
146
132
  return typeof first === 'function' ? subscribe(state, first) : noop;
147
133
  },
148
- unshift: (...items: Item[]) => state.value.unshift(...items),
149
134
  unsubscribe: (first: number | GenericCallback, second?: GenericCallback) => {
150
135
  if (typeof first === 'number' && typeof second === 'function') {
151
136
  getReactiveValueInProxy(instance as ReactiveArray<Item>, indiced, first, true)?.unsubscribe(
@@ -155,6 +140,36 @@ export function array<Item>(
155
140
  unsubscribe(state, first);
156
141
  }
157
142
  },
143
+ };
144
+
145
+ instance = {
146
+ ...handlers,
147
+ asReadonly: (frozen?: unknown) =>
148
+ getReadonlyInstance(state, readonlies, handlers, frozen === true),
149
+ at: (index: number): Item | undefined => get(index),
150
+ clear: () => {
151
+ state.value.length = 0;
152
+ },
153
+ filter: (callback: (item: Item, index: number, array: Item[]) => boolean) =>
154
+ computed(() => filter(get(), callback)),
155
+ map: <Mapped>(callback: (item: Item, index: number, array: Item[]) => Mapped) =>
156
+ computed(() => get().map(callback)),
157
+ notify: () => {
158
+ emityProxyValues(state, indiced);
159
+ },
160
+ pop: () => state.value.pop(),
161
+ push: (...items: Item[]) => state.value.push(...items),
162
+ select: <Mapped>(
163
+ filter: (item: Item, index: number, array: Item[]) => boolean,
164
+ map: (item: Item, index: number, array: Item[]) => Mapped,
165
+ ) => computed(() => select(get() as Item[], filter, map)),
166
+ set: (first?: unknown, second?: unknown) => {
167
+ set(first, second);
168
+ },
169
+ shift: () => state.value.shift(),
170
+ splice: (from: number, to?: number, ...items: Item[]) =>
171
+ state.value.splice(from, to ?? state.value.length, ...items),
172
+ unshift: (...items: Item[]) => state.value.unshift(...items),
158
173
  update: (callback: (value: Item[]) => Item[]) =>
159
174
  updateArrayValue(instance as ReactiveArray<Item>, state, callback),
160
175
  };
@@ -171,7 +186,7 @@ export function array<Item>(
171
186
  },
172
187
  });
173
188
 
174
- instance.set(value);
189
+ set(value);
175
190
 
176
191
  return Object.freeze(instance) as ReactiveArray<Item>;
177
192
  }
@@ -201,13 +216,34 @@ function isArrayValue<Item>(value: unknown): value is Item[] | undefined {
201
216
  function peekArrayValue<Item>(
202
217
  state: ReactiveState<Item[], Item>,
203
218
  length: Signal<number>,
204
- value?: unknown,
219
+ first?: unknown,
220
+ second?: boolean,
205
221
  ): number | Item | Item[] | undefined {
206
- if (value === PROPERTY_LENGTH) {
222
+ if (first === PROPERTY_LENGTH) {
207
223
  return length.peek();
208
224
  }
209
225
 
210
- return typeof value === 'number' ? state.value.at(value) : state.value.slice();
226
+ let value: Item | Item[] | undefined;
227
+
228
+ if (typeof first === 'number') {
229
+ value = state.value.at(first);
230
+ } else {
231
+ value = state.value;
232
+ }
233
+
234
+ if (!(first === true || second === true)) {
235
+ return value;
236
+ }
237
+
238
+ if (Array.isArray(value)) {
239
+ return value.slice();
240
+ }
241
+
242
+ if (isPlainObject(value)) {
243
+ return {...value};
244
+ }
245
+
246
+ return value;
211
247
  }
212
248
 
213
249
  function setArray<Item>(state: ReactiveState<Item[], Item>, value: Item[] | undefined): void {
@@ -0,0 +1,71 @@
1
+ import {isPlainObject} from '@oscarpalmer/atoms/is';
2
+ import type {GenericCallback} from '@oscarpalmer/atoms/models';
3
+ import {NAME_MORA, NAME_READONLY} from '../constants';
4
+ import {getSimpleValue} from '../helpers/value';
5
+ import type {
6
+ ReactiveState,
7
+ ReadonlyFrozenSignal,
8
+ ReadonlyInstances,
9
+ ReadonlySignal,
10
+ ReadonlySignalValue,
11
+ } from '../models';
12
+
13
+ export function getReadonlyInstance<Value>(
14
+ state: ReactiveState<Value, never>,
15
+ instances: ReadonlyInstances<Value>,
16
+ handlers: Record<string, GenericCallback>,
17
+ frozen: boolean,
18
+ ): ReadonlySignal<Value> {
19
+ const key = frozen ? 'frozen' : 'original';
20
+
21
+ instances[key] ??= getReadonlySignal(state, handlers, frozen) as never;
22
+
23
+ return instances[key] as ReadonlySignal<Value>;
24
+ }
25
+
26
+ export function getReadonlySignal<Value>(
27
+ state: ReactiveState<Value, never>,
28
+ handlers: Record<string, GenericCallback>,
29
+ frozen: boolean,
30
+ ): ReadonlySignal<Value> | ReadonlyFrozenSignal<Value> {
31
+ const instance = {
32
+ ...handlers,
33
+ get: () => getReadonlyValue(state, false, frozen),
34
+ peek: () => getReadonlyValue(state, true, frozen),
35
+ };
36
+
37
+ Object.defineProperties(instance, {
38
+ [NAME_MORA]: {
39
+ enumerable: false,
40
+ value: NAME_READONLY,
41
+ },
42
+ frozen: {
43
+ enumerable: true,
44
+ value: frozen,
45
+ },
46
+ });
47
+
48
+ return Object.freeze(instance) as ReadonlySignal<Value>;
49
+ }
50
+
51
+ function getReadonlyValue<Value>(
52
+ state: ReactiveState<Value, never>,
53
+ peek: boolean,
54
+ frozen: boolean,
55
+ ): Value | ReadonlySignalValue<Value> {
56
+ let value = peek ? state.value : getSimpleValue(state);
57
+
58
+ if (!frozen) {
59
+ return value;
60
+ }
61
+
62
+ if (Array.isArray(state.value)) {
63
+ value = [...state.value] as Value;
64
+ } else if (isPlainObject(state.value)) {
65
+ value = {...state.value} as Value;
66
+ } else {
67
+ value = state.value;
68
+ }
69
+
70
+ return Object.freeze(value);
71
+ }
@@ -1,8 +1,9 @@
1
1
  import {NAME_MORA, NAME_SIGNAL} from '../constants';
2
2
  import {emitValue, getSimpleValue, handleSimpleValue} from '../helpers/value';
3
- import type {ReactiveOptions, ReactiveState, Signal} from '../models';
3
+ import type {ReactiveOptions, ReactiveState, ReadonlyInstances, Signal} from '../models';
4
4
  import {subscribe} from '../subscription';
5
5
  import {reactive} from './reactive';
6
+ import {getReadonlyInstance} from './readonly';
6
7
 
7
8
  function setAndEmit<Value>(state: ReactiveState<Value, Value>, value: Value): void {
8
9
  if (!state.equal(state.value, value)) {
@@ -51,8 +52,20 @@ export function signal<Value>(
51
52
  ): Signal<Value> {
52
53
  const [rx, state] = reactive<Value>(undefined as unknown as Value, options);
53
54
 
54
- const instance = {
55
+ const readonlies: ReadonlyInstances<Value> = {};
56
+
57
+ const handlers = {
55
58
  ...rx,
59
+ subscribe: (callback: (value: Value) => void) => subscribe(state, callback),
60
+ unsubscribe: (callback: (value: Value) => void) => {
61
+ state.subscriptions.delete(callback);
62
+ },
63
+ };
64
+
65
+ const instance = {
66
+ ...handlers,
67
+ asReadonly: (frozen?: unknown) =>
68
+ getReadonlyInstance(state, readonlies, handlers, frozen === true),
56
69
  get: () => getSimpleValue(state),
57
70
  peek: () => state.value,
58
71
  set: (value: Value | (() => Value | Promise<Value>) | Promise<Value>) => {
@@ -61,10 +74,6 @@ export function signal<Value>(
61
74
  update: (callback: (value: Value) => Value) => {
62
75
  handleSimpleValue(state, callback(state.value), setAndEmit);
63
76
  },
64
- subscribe: (callback: (value: Value) => void) => subscribe(state, callback),
65
- unsubscribe: (callback: (value: Value) => void) => {
66
- state.subscriptions.delete(callback);
67
- },
68
77
  };
69
78
 
70
79
  Object.defineProperty(instance, NAME_MORA, {
@@ -15,10 +15,12 @@ import type {
15
15
  ReactiveOptions,
16
16
  ReactiveState,
17
17
  ReactiveStore,
18
+ ReadonlyInstances,
18
19
  Unsubscribe,
19
20
  } from '../models';
20
21
  import {noop, subscribe, unsubscribe} from '../subscription';
21
22
  import {reactive} from './reactive';
23
+ import {getReadonlyInstance} from './readonly';
22
24
 
23
25
  function isStoreObject<Value extends PlainObject>(value: unknown): value is Value | undefined {
24
26
  return value == null || isPlainObject(value);
@@ -56,6 +58,34 @@ function setObject(state: ReactiveState<PlainObject, never>, value: PlainObject
56
58
  stopBatch();
57
59
  }
58
60
 
61
+ function peekStoreValue<Value extends PlainObject>(
62
+ state: ReactiveState<Value, Value>,
63
+ first?: unknown,
64
+ second?: boolean,
65
+ ): unknown {
66
+ let value: unknown;
67
+
68
+ if (isKey(first)) {
69
+ value = state.value[first];
70
+ } else {
71
+ value = state.value;
72
+ }
73
+
74
+ if (!(first === true || second === true)) {
75
+ return value;
76
+ }
77
+
78
+ if (Array.isArray(value)) {
79
+ return value.slice();
80
+ }
81
+
82
+ if (isPlainObject(value)) {
83
+ return {...value};
84
+ }
85
+
86
+ return value;
87
+ }
88
+
59
89
  function setProperty<Value extends PlainObject>(
60
90
  state: ReactiveState<Value, Value>,
61
91
  key: unknown,
@@ -107,6 +137,9 @@ export function store<Value extends PlainObject>(
107
137
  const [rx, state] = reactive<Value>(undefined as never, options);
108
138
 
109
139
  const keyed = new Map<Key, [Computed<unknown>, ComputedEffect]>();
140
+ const readonlies: ReadonlyInstances<Value> = {};
141
+
142
+ let instance: Record<string, GenericCallback> = {};
110
143
 
111
144
  state.value = new Proxy({} as Value, {
112
145
  set: (target: Value, property: PropertyKey, value: unknown) =>
@@ -119,16 +152,43 @@ export function store<Value extends PlainObject>(
119
152
  }),
120
153
  });
121
154
 
122
- const instance = {
155
+ const handlers = {
123
156
  ...rx,
124
157
  get: (value?: unknown): unknown =>
125
158
  isKey(value)
126
- ? getReactiveValueInProxy(instance, keyed, value, false).get()
159
+ ? getReactiveValueInProxy(instance as ReactiveStore<Value>, keyed, value, false).get()
127
160
  : getSimpleValue(state),
161
+ peek: (first?: unknown, second?: boolean): unknown => peekStoreValue(state, first, second),
162
+ subscribe: (first: Key | GenericCallback, second?: GenericCallback): Unsubscribe => {
163
+ if (isKey(first) && typeof second === 'function') {
164
+ return getReactiveValueInProxy(
165
+ instance as ReactiveStore<Value>,
166
+ keyed,
167
+ first,
168
+ false,
169
+ ).subscribe(second);
170
+ }
171
+
172
+ return typeof first === 'function' ? subscribe(state, first) : noop;
173
+ },
174
+ unsubscribe: (first: Key | GenericCallback, second?: GenericCallback) => {
175
+ if (isKey(first) && typeof second === 'function') {
176
+ getReactiveValueInProxy(instance as ReactiveStore<Value>, keyed, first, false)?.unsubscribe(
177
+ second,
178
+ );
179
+ } else if (typeof first === 'function') {
180
+ unsubscribe(state, first);
181
+ }
182
+ },
183
+ };
184
+
185
+ instance = {
186
+ ...handlers,
187
+ asReadonly: (frozen?: unknown) =>
188
+ getReadonlyInstance(state, readonlies, handlers, frozen === true),
128
189
  notify(): void {
129
190
  emityProxyValues(state, keyed);
130
191
  },
131
- peek: (value?: unknown): unknown => (isKey(value) ? state.value[value] : {...state.value}),
132
192
  set: (first?: unknown, second?: unknown) => {
133
193
  setProxyValue<Value>(
134
194
  false,
@@ -141,20 +201,6 @@ export function store<Value extends PlainObject>(
141
201
  second,
142
202
  );
143
203
  },
144
- subscribe: (first: Key | GenericCallback, second?: GenericCallback): Unsubscribe => {
145
- if (isKey(first) && typeof second === 'function') {
146
- return getReactiveValueInProxy(instance, keyed, first, false).subscribe(second);
147
- }
148
-
149
- return typeof first === 'function' ? subscribe(state, first) : noop;
150
- },
151
- unsubscribe: (first: Key | GenericCallback, second?: GenericCallback) => {
152
- if (isKey(first) && typeof second === 'function') {
153
- getReactiveValueInProxy(instance, keyed, first, false)?.unsubscribe(second);
154
- } else if (typeof first === 'function') {
155
- unsubscribe(state, first);
156
- }
157
- },
158
204
  update: (callback: (value: Value) => Value) => {
159
205
  const updated = callback(state.value);
160
206