@mmstack/primitives 22.10.4 → 22.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -369,8 +369,11 @@ The foundation of stale-while-revalidate. Wraps a signal so it **holds its last
369
369
  import { keepPrevious } from '@mmstack/primitives';
370
370
 
371
371
  const held = keepPrevious(resource.value); // drops to undefined mid-reload → keeps last value
372
+ const rows = keepPrevious(resource.value, { fallback: [] }); // [] only until the first value lands
372
373
  ```
373
374
 
375
+ `fallback` is yielded only while nothing has ever been defined; after the first defined value the previous value covers every gap, never the fallback. Like any linked signal the hold is lazy: it carries a value it has computed with, so place it over the value your readers read — reading is what feeds it. `@mmstack/resource` does exactly that for its `keepPrevious` option.
376
+
374
377
  If the source is writable, `set` / `update` / `asReadonly` (and `mutate` / `inline` / `from` for mutable / derived sources) are forwarded through, so it stays a drop-in replacement. `@mmstack/resource` uses it under the hood for its `keepPrevious` option.
375
378
 
376
379
  ### Keep-alive — `MmActivity` / `injectPaused` / `providePaused`
@@ -218,10 +218,10 @@ class MmActivity {
218
218
  else
219
219
  this.view.detach();
220
220
  }
221
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: MmActivity, deps: [], target: i0.ɵɵFactoryTarget.Directive });
222
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: MmActivity, isStandalone: true, selector: "[mmActivity]", inputs: { visible: { classPropertyName: "visible", publicName: "mmActivity", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
221
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: MmActivity, deps: [], target: i0.ɵɵFactoryTarget.Directive });
222
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.4", type: MmActivity, isStandalone: true, selector: "[mmActivity]", inputs: { visible: { classPropertyName: "visible", publicName: "mmActivity", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
223
223
  }
224
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: MmActivity, decorators: [{
224
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: MmActivity, decorators: [{
225
225
  type: Directive,
226
226
  args: [{
227
227
  selector: '[mmActivity]',
@@ -1129,10 +1129,10 @@ class SuspenseBoundaryBase {
1129
1129
  pending = this.scope.pending;
1130
1130
  suspended = computed(() => this.scope.suspended(this.type()), /* @ts-ignore */
1131
1131
  ...(ngDevMode ? [{ debugName: "suspended" }] : /* istanbul ignore next */ []));
1132
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: SuspenseBoundaryBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1133
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: SuspenseBoundaryBase, isStandalone: true, inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
1132
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: SuspenseBoundaryBase, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1133
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.4", type: SuspenseBoundaryBase, isStandalone: true, inputs: { type: { classPropertyName: "type", publicName: "type", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0 });
1134
1134
  }
1135
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: SuspenseBoundaryBase, decorators: [{
1135
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: SuspenseBoundaryBase, decorators: [{
1136
1136
  type: Directive
1137
1137
  }], propDecorators: { type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }] } });
1138
1138
  const SUSPENSE_TEMPLATE = `
@@ -1160,10 +1160,10 @@ const SUSPENSE_HOST = {
1160
1160
  * `provideTransitionScope()`. The common case.
1161
1161
  */
1162
1162
  class SuspenseBoundary extends SuspenseBoundaryBase {
1163
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: SuspenseBoundary, deps: null, target: i0.ɵɵFactoryTarget.Component });
1164
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: SuspenseBoundary, isStandalone: true, selector: "mm-suspense", host: { properties: { "attr.aria-busy": "pending() ? true : null" } }, providers: [provideTransitionScope()], usesInheritance: true, ngImport: i0, template: "\n @if (suspended()) {\n <ng-content select=\"[placeholder]\"><span>Loading\u2026</span></ng-content>\n } @else {\n @if (pending()) {\n <ng-content select=\"[busy]\" />\n }\n <ng-content />\n }\n", isInline: true, styles: [":host{display:contents}\n"] });
1163
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: SuspenseBoundary, deps: null, target: i0.ɵɵFactoryTarget.Component });
1164
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: SuspenseBoundary, isStandalone: true, selector: "mm-suspense", host: { properties: { "attr.aria-busy": "pending() ? true : null" } }, providers: [provideTransitionScope()], usesInheritance: true, ngImport: i0, template: "\n @if (suspended()) {\n <ng-content select=\"[placeholder]\"><span>Loading\u2026</span></ng-content>\n } @else {\n @if (pending()) {\n <ng-content select=\"[busy]\" />\n }\n <ng-content />\n }\n", isInline: true, styles: [":host{display:contents}\n"] });
1165
1165
  }
1166
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: SuspenseBoundary, decorators: [{
1166
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: SuspenseBoundary, decorators: [{
1167
1167
  type: Component,
1168
1168
  args: [{ selector: 'mm-suspense', template: SUSPENSE_TEMPLATE, host: SUSPENSE_HOST, providers: [provideTransitionScope()], styles: [":host{display:contents}\n"] }]
1169
1169
  }] });
@@ -1174,10 +1174,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
1174
1174
  * ancestor.
1175
1175
  */
1176
1176
  class UnscopedSuspenseBoundary extends SuspenseBoundaryBase {
1177
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: UnscopedSuspenseBoundary, deps: null, target: i0.ɵɵFactoryTarget.Component });
1178
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: UnscopedSuspenseBoundary, isStandalone: true, selector: "mm-unscoped-suspense", host: { properties: { "attr.aria-busy": "pending() ? true : null" } }, usesInheritance: true, ngImport: i0, template: "\n @if (suspended()) {\n <ng-content select=\"[placeholder]\"><span>Loading\u2026</span></ng-content>\n } @else {\n @if (pending()) {\n <ng-content select=\"[busy]\" />\n }\n <ng-content />\n }\n", isInline: true, styles: [":host{display:contents}\n"] });
1177
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UnscopedSuspenseBoundary, deps: null, target: i0.ɵɵFactoryTarget.Component });
1178
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: UnscopedSuspenseBoundary, isStandalone: true, selector: "mm-unscoped-suspense", host: { properties: { "attr.aria-busy": "pending() ? true : null" } }, usesInheritance: true, ngImport: i0, template: "\n @if (suspended()) {\n <ng-content select=\"[placeholder]\"><span>Loading\u2026</span></ng-content>\n } @else {\n @if (pending()) {\n <ng-content select=\"[busy]\" />\n }\n <ng-content />\n }\n", isInline: true, styles: [":host{display:contents}\n"] });
1179
1179
  }
1180
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: UnscopedSuspenseBoundary, decorators: [{
1180
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: UnscopedSuspenseBoundary, decorators: [{
1181
1181
  type: Component,
1182
1182
  args: [{ selector: 'mm-unscoped-suspense', template: SUSPENSE_TEMPLATE, host: SUSPENSE_HOST, styles: [":host{display:contents}\n"] }]
1183
1183
  }] });
@@ -1440,10 +1440,10 @@ class MmTransition {
1440
1440
  node.style.display = hidden ? 'none' : '';
1441
1441
  }
1442
1442
  }
1443
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: MmTransition, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1444
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: MmTransition, isStandalone: true, selector: "[mmTransition]", inputs: { value: { classPropertyName: "value", publicName: "mmTransition", isSignal: true, isRequired: true, transformFunction: null }, immediate: { classPropertyName: "immediate", publicName: "mmTransitionImmediate", isSignal: true, isRequired: false, transformFunction: null }, viewTransition: { classPropertyName: "viewTransition", publicName: "mmTransitionViewTransition", isSignal: true, isRequired: false, transformFunction: null } }, exportAs: ["mmTransition"], ngImport: i0 });
1443
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: MmTransition, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1444
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.4", type: MmTransition, isStandalone: true, selector: "[mmTransition]", inputs: { value: { classPropertyName: "value", publicName: "mmTransition", isSignal: true, isRequired: true, transformFunction: null }, immediate: { classPropertyName: "immediate", publicName: "mmTransitionImmediate", isSignal: true, isRequired: false, transformFunction: null }, viewTransition: { classPropertyName: "viewTransition", publicName: "mmTransitionViewTransition", isSignal: true, isRequired: false, transformFunction: null } }, exportAs: ["mmTransition"], ngImport: i0 });
1445
1445
  }
1446
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: MmTransition, decorators: [{
1446
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: MmTransition, decorators: [{
1447
1447
  type: Directive,
1448
1448
  args: [{
1449
1449
  selector: '[mmTransition]',
@@ -1488,10 +1488,10 @@ class MmViewTransitionName {
1488
1488
  el.style.removeProperty('view-transition-name');
1489
1489
  });
1490
1490
  }
1491
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: MmViewTransitionName, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1492
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.0.2", type: MmViewTransitionName, isStandalone: true, selector: "[mmViewTransitionName]", inputs: { mmViewTransitionName: { classPropertyName: "mmViewTransitionName", publicName: "mmViewTransitionName", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
1491
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: MmViewTransitionName, deps: [], target: i0.ɵɵFactoryTarget.Directive });
1492
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "22.1.4", type: MmViewTransitionName, isStandalone: true, selector: "[mmViewTransitionName]", inputs: { mmViewTransitionName: { classPropertyName: "mmViewTransitionName", publicName: "mmViewTransitionName", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0 });
1493
1493
  }
1494
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: MmViewTransitionName, decorators: [{
1494
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: MmViewTransitionName, decorators: [{
1495
1495
  type: Directive,
1496
1496
  args: [{ selector: '[mmViewTransitionName]' }]
1497
1497
  }], ctorParameters: () => [], propDecorators: { mmViewTransitionName: [{ type: i0.Input, args: [{ isSignal: true, alias: "mmViewTransitionName", required: true }] }] } });
@@ -1890,14 +1890,19 @@ function isDerivation(sig) {
1890
1890
 
1891
1891
  function keepPrevious(src, opt) {
1892
1892
  const mutableSrc = isWritableSignal$2(src) && isMutable(src);
1893
+ const { fallback, ...signalOpt } = opt ?? {};
1893
1894
  let cnt = 0;
1894
1895
  const baseEqual = opt?.equal;
1895
1896
  const equal = mutableSrc
1896
1897
  ? (a, b) => cnt > 0 ? false : baseEqual ? baseEqual(a, b) : Object.is(a, b)
1897
1898
  : baseEqual;
1898
- const persisted = linkedSignal({ ...(ngDevMode ? { debugName: "persisted" } : /* istanbul ignore next */ {}), ...opt,
1899
+ const persisted = linkedSignal({ ...(ngDevMode ? { debugName: "persisted" } : /* istanbul ignore next */ {}), ...signalOpt,
1899
1900
  source: () => src(),
1900
- computation: (next, prev) => next === undefined && prev !== undefined ? prev.value : next,
1901
+ computation: (next, prev) => {
1902
+ if (next !== undefined)
1903
+ return next;
1904
+ return prev !== undefined ? prev.value : fallback;
1905
+ },
1901
1906
  equal });
1902
1907
  if (isWritableSignal$2(src)) {
1903
1908
  persisted.set = src.set;
@@ -5152,6 +5157,81 @@ function validateEnvelope(env) {
5152
5157
  }
5153
5158
  return null;
5154
5159
  }
5160
+ function wireValueViolation(value) {
5161
+ return violationAt(value, false, []);
5162
+ }
5163
+ function violationAt(value, nested, ancestors) {
5164
+ if (value === undefined)
5165
+ return nested ? 'undefined-in-container' : null;
5166
+ if (value === null)
5167
+ return null;
5168
+ const t = typeof value;
5169
+ if (t === 'boolean' || t === 'string')
5170
+ return null;
5171
+ if (t === 'number') {
5172
+ if (!Number.isFinite(value))
5173
+ return 'non-finite-number';
5174
+ return Object.is(value, -0) ? 'negative-zero' : null;
5175
+ }
5176
+ if (t === 'bigint')
5177
+ return 'bigint';
5178
+ if (t === 'function')
5179
+ return 'function';
5180
+ if (t === 'symbol')
5181
+ return 'symbol';
5182
+ if (ancestors.includes(value))
5183
+ return 'cycle';
5184
+ ancestors.push(value);
5185
+ try {
5186
+ if (Array.isArray(value)) {
5187
+ for (let i = 0; i < value.length; i++) {
5188
+ if (!Object.hasOwn(value, i))
5189
+ return 'sparse-array';
5190
+ const reason = violationAt(value[i], true, ancestors);
5191
+ if (reason)
5192
+ return reason;
5193
+ }
5194
+ const keys = Object.keys(value);
5195
+ if (keys.length !== value.length)
5196
+ return 'array-named-property';
5197
+ for (const key of keys) {
5198
+ const idx = Number(key);
5199
+ if (String(idx) !== key ||
5200
+ !Number.isInteger(idx) ||
5201
+ idx < 0 ||
5202
+ idx >= value.length) {
5203
+ return 'array-named-property';
5204
+ }
5205
+ }
5206
+ return null;
5207
+ }
5208
+ const proto = Object.getPrototypeOf(value);
5209
+ if (proto !== Object.prototype && proto !== null)
5210
+ return 'non-plain-object';
5211
+ for (const key of Object.keys(value)) {
5212
+ const reason = violationAt(value[key], true, ancestors);
5213
+ if (reason)
5214
+ return reason;
5215
+ }
5216
+ return null;
5217
+ }
5218
+ finally {
5219
+ ancestors.pop();
5220
+ }
5221
+ }
5222
+ // dev-only: one warning per offending batch names the first violating op, then stops
5223
+ const lintWireValues = (ops) => {
5224
+ for (const op of ops) {
5225
+ const reason = (op.kind === 'set' ? wireValueViolation(op.next) : null) ??
5226
+ (op.kind !== 'clear' && Object.hasOwn(op, 'prev')
5227
+ ? wireValueViolation(op.prev)
5228
+ : null);
5229
+ if (!reason)
5230
+ continue;
5231
+ console.warn(`[@mmstack/primitives] op value at "${op.path.join('.')}" will not survive a JSON transport (${reason}): peers materialize a different value than this emitter, and the room can diverge. Op values must round-trip JSON unchanged; encode rich leaves as strings.`);
5232
+ return;
5233
+ }
5234
+ };
5155
5235
  const lww = (_ancestor, mine) => mine;
5156
5236
  const mergeThree = (ancestor, mine, theirs) => merge3(ancestor, mine, theirs);
5157
5237
  const preserve = (ancestor, mine, theirs) => ({
@@ -5700,6 +5780,17 @@ function createConvergingApply(opt) {
5700
5780
  const reg = registers.get(keyOf$1(path));
5701
5781
  return reg ? liveOf(reg) : [];
5702
5782
  },
5783
+ liveUnder: (path) => {
5784
+ const key = keyOf$1(path);
5785
+ const out = [];
5786
+ const at = registers.get(key);
5787
+ for (const reg of at ? [at, ...descendantsOf(key)] : descendantsOf(key)) {
5788
+ const siblings = liveOf(reg);
5789
+ if (siblings.length)
5790
+ out.push({ path: reg.path, siblings });
5791
+ }
5792
+ return out;
5793
+ },
5703
5794
  materialize: () => {
5704
5795
  const root = registers.get('');
5705
5796
  const res = root?.result;
@@ -5906,6 +5997,8 @@ function opSync(source, opt) {
5906
5997
  const emitLocal = (ops) => {
5907
5998
  const frontier = scopeFrontier;
5908
5999
  const stamped = conv.stamp(ops, { bump: bumping, frontier });
6000
+ if (isDevMode())
6001
+ lintWireValues(stamped);
5909
6002
  const nextVersion = (versions.get(origin) ?? 0) + 1;
5910
6003
  const env = {
5911
6004
  proto: OP_PROTO_VERSION,
@@ -6010,6 +6103,7 @@ function opSync(source, opt) {
6010
6103
  log.flush(); // fold pending base writes in first, so they count as observed
6011
6104
  return conv.captureFrontier();
6012
6105
  },
6106
+ liveUnder: (path) => conv.liveUnder(path),
6013
6107
  commitScope: (frontier, fn) => {
6014
6108
  log.flush(); // earlier pending writes emit against the live frontier, not this one
6015
6109
  scopeFrontier = frontier;
@@ -7037,10 +7131,10 @@ class MessageBus {
7037
7131
  post: (value) => this.channel.postMessage({ id, value }),
7038
7132
  };
7039
7133
  }
7040
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: MessageBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
7041
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: MessageBus, providedIn: 'root' });
7134
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: MessageBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
7135
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: MessageBus, providedIn: 'root' });
7042
7136
  }
7043
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: MessageBus, decorators: [{
7137
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: MessageBus, decorators: [{
7044
7138
  type: Injectable,
7045
7139
  args: [{
7046
7140
  providedIn: 'root',