npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@selfage/observable

v1.0.2

Published

Runtime lib for generated observables.

Downloads

24

Readme

@selfage/observable

Install

npm install @selfage/observable

Overview

Written in TypeScript and compiled to ES6 with inline source map & source. See @selfage/tsconfig for full compiler options. Provides a runtime lib to be used together with ObservableDescriptor generated by @selfage/generator_cli, which can parse, copy and merge observable objects.

An observable object exposes events/callbacks to observe every state change.

Example generated code

See @selfage/generator_cli#observable for how to generate ObservableDescriptor. Suppose the following has been generated and committed as basic.ts. We will continue using the example below.

import { ObservableArray } from '@selfage/observable_array';
import { EventEmitter } from 'events';
import { ObservableDescriptor, ArrayType } from '@selfage/observable/descriptor';
import { PrimitiveType } from '@selfage/message/descriptor';

export interface BasicData {
  on(event: 'numberField', listener: (newValue: number, oldValue: number) => void): this;
  on(event: 'stringArrayField', listener: (newValue: Array<string>, oldValue: Array<string>) => void): this;
  on(event: 'observableArrayField', listener: (newValue: ObservableArray<boolean>, oldValue: ObservableArray<boolean>) => void): this;
  on(event: 'init', listener: () => void): this;
}

export class BasicData extends EventEmitter {
  private numberField_?: number;
  get numberField(): number {
    return this.numberField_;
  }
  set numberField(value: number) {
    let oldValue = this.numberField_;
    if (value === oldValue) {
      return;
    }
    this.numberField_ = value;
    this.emit('numberField', this.numberField_, oldValue);
  }

  private stringArrayField_?: Array<string>;
  get stringArrayField(): Array<string> {
    return this.stringArrayField_;
  }
  set stringArrayField(value: Array<string>) {
    let oldValue = this.stringArrayField_;
    if (value === oldValue) {
      return;
    }
    this.stringArrayField_ = value;
    this.emit('stringArrayField', this.stringArrayField_, oldValue);
  }

  private observableArrayField_?: ObservableArray<boolean>;
  get observableArrayField(): ObservableArray<boolean> {
    return this.observableArrayField_;
  }
  set observableArrayField(value: ObservableArray<boolean>) {
    let oldValue = this.observableArrayField_;
    if (value === oldValue) {
      return;
    }
    this.observableArrayField_ = value;
    this.emit('observableArrayField', this.observableArrayField_, oldValue);
  }

  public triggerInitialEvents(): void {
    if (this.numberField_ !== undefined) {
      this.emit('numberField', this.numberField_, undefined);
    }
    if (this.stringArrayField_ !== undefined) {
      this.emit('stringArrayField', this.stringArrayField_, undefined);
    }
    if (this.observableArrayField_ !== undefined) {
      this.emit('observableArrayField', this.observableArrayField_, undefined);
    }
    this.emit('init');
  }

  public toJSON(): Object {
    return {
      numberField: this.numberField,
      stringArrayField: this.stringArrayField,
      observableArrayField: this.observableArrayField,
    };
  }
}

export let BASIC_DATA: ObservableDescriptor<BasicData> = {
  name: 'BasicData',
  constructor: BasicData,
  fields: [
    {
      name: 'numberField',
      primitiveType: PrimitiveType.NUMBER,
    },
    {
      name: 'stringArrayField',
      primitiveType: PrimitiveType.STRING,
      asArray: ArrayType.NORMAL,
    },
    {
      name: 'observableArrayField',
      primitiveType: PrimitiveType.BOOLEAN,
      asArray: ArrayType.OBSERVABLE,
    },
  ]
};

Listen on observable object

Changes are detected through TypeScript setter. Events are emitted via NodeJs's EventEmitter.

import { BasicData } from './basic'; // Generated by @selfage/generator_cli.
import { ObservableArray } from '@selfage/observable_array';

let basicData = new BasicData();
basicData.on('numberField', (newValue, oldValue) => {
  console.log(`newValue: ${newValue}; oldValue: ${oldValue};`);
});
basicData.numberField = 10;
// Print: newValue: 10; oldValue: undefined;
basicData.numberField = 100;
// Print: newValue: 100; oldValue: 10;
delete basicData.numberField;
// Actually does nothing. basicData.numberField is still 100.
basicData.numberField = undefined;
// Print: newValue: undefined; oldValue: 100;

basicData.on('stringArrayField', (newValue, oldValue) => {
  console.log(`newValue: ${JSON.stringify(newValue)}; oldValue: ${JSON.stringify(oldValue)};`);
});
basicData.stringArrayField = ['str1', 'str2'];
// Print: newValue: ['str1','str2']; oldValue: undefined;
basicData.stringArrayField = ['str1', 'str2'];
// Print: newValue: ['str1','str2']; oldValue: ['str1','str2'];
// This is because the new and old ObservableArray's are not the instance. I.e., they are not equal by `===`.
basicData.stringArrayField.push('str3');
// Nothing to print as changes are not bubbled up.

basicData.on('observableArrayField', (newValue, oldValue) => {
  console.log(`newValue: ${JSON.stringify(newValue)}; oldValue: ${JSON.stringify(oldValue)};`);
});
basicData.observableArrayField = ObservableArray.of(true, false);
// Print: newValue: [true,false]; oldValue: undefined;
basicData.observableArrayField.push(false);
// Nothing to print as changes are not bubbled up.

Note that changes on arrays or objects are not bubbled up.

In order to observe arrays, you need to add a listener on basicData.observableArrayField directly. Refer to package @selfage/observable_array for how to observe an ObservableArray.

Similarly, if you nest BasicData inside another observable object, you need to add listeners on nested observable objects directly.

Trigger initial events

If you have created an observable object before you could add listeners to it, you can trigger initial events, such that listeners called as if each field is just assigned with the new value.

import { BASIC_DATA, BasicData } from './basic'; // Generated by @selfage/generator_cli.

let data = new BasicData();
data.numberField = 111;
data.triggerInitialEvents();
// Emit `numberField` event with newValue as 111, and oldValue as undefined.
// A special 'init' event will also be triggered which passes nothing to the listener. It can be used to flip undefined fields.

Parse observables

You might not create an observable object directly, but parse a JSON-parsed object as the following.

import { parseObservable } from '@selfage/observable/parser';
import { BASIC_DATA, BasicData } from './basic'; // Generated by @selfage/generator_cli.

let raw = JSON.parse(`{ "numberField": 111, "otherField": "random", "stringArrayField": ["str1", "str2"] }`);
let basicData = parseObservable(raw, BASIC_DATA); // Of type `BasicData`.

You can also supply an in-place output object.

let output = new BasicData();
parseObservable(raw, BASIC_DATA, output);

Note that it will overwrite everything in output.

Copy observables

You can copy observables.

import { copyObservable } from '@selfage/observable/copier';
import { BASIC_DATA, BasicData } from './basic'; // Generated by @selfage/generator_cli.

let basicData = new BasicData();
basic.numberField = 100;
let dest = copyObservable(basicData, BASIC_DATA);
// Or in-place copy.
let dest2 = new BasicData();
copyObservable(data, BASIC_DATA, dest2);

Merge observables

If provided with a destination/existing observable object, both parseObservable and copyObservable will replace every field with the new one. mergeObservable, however, will only overwrite a field if the corresponding new field actually has a value.

import { mergeObservable } from '@selfage/observable/merger';
import { BASIC_DATA, BasicData } from './basic'; // Generated by @selfage/generator_cli.

let source = new BasicData();
source.stringArrayField = ["123"];
let existing = new BasicData();
existing.numberField = 111;
mergeMessage(source, BASIC_DATA, existing);
// Now `existing` becomes: { numberField: 111, stringArrayField: ["123"] }

Test matcher

By importing @selfage/observable/test_matcher, you can use it together with @selfage/test_matcher to match messages.

import { BasicData, BASIC_DATA } from './basic'; // Generated by @selfage/generator_cli.
import { eqObservable } from '@selfage/observable/test_matcher';
import { assertThat } from '@selfage/test_matcher'; // Install @selfage/test_matcher

let basicData = new BasicData();
basic.numberField = 111;
let expectedData = new BasicData();
assertThat(basicData, eqObservable(expectedData, BASIC_DATA), `basicData`);

Design considerations for observable object

We have also provided @selfage/observable_js in pure JavaScript to convert any objects into observable objects via ES6 proxy. The main reason we didn't do the same thing in TypeScript is that we failed to find a way to make the converted observable objects type-safe. I.e., what would be the return type for function toObservable<T>(obj: T): ? requring on(event: '<field name>', listener:...) to be added to T and can be type checked by TypeScript?

As for why we didn't allow bubbling up changes, it's because:

  1. Our main use case is to observe changes on states to trigger UI changes, where each component can own its own observable object. Nested objects should be observed by nested components. It could be messy to ignore nested objects.
  2. If you want to push new states into browser history, you probably don't want to push upon every single change, because an operation might trigger multiple changes which should be grouped into one history entry.