@zoobzio/kit 0.0.0-stage → 0.0.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.
@@ -0,0 +1,94 @@
1
+ /**
2
+ * One middleware slot per field of a state container `S`. A slot receives
3
+ * the value of its field and returns the value the service uses. A missing
4
+ * slot passes the value on.
5
+ */
6
+ export type Middleware<S> = { [K in keyof S]?: (value: S[K]) => S[K]; };
7
+ /**
8
+ * The middleware for reads and writes of a state container.
9
+ */
10
+ export type Options<S> = {
11
+ get?: {
12
+ state?: Middleware<S>;
13
+ };
14
+ set?: {
15
+ state?: Middleware<S>;
16
+ };
17
+ };
18
+ /**
19
+ * The fields of a state container, as a record keyed by field name. The
20
+ * proxy uses the keys to tell a container field from any other key.
21
+ */
22
+ export type Fields<S> = Record<keyof S, null>;
23
+ /**
24
+ * The change object of each field: the field name with the stored values
25
+ * around the write, after the `set` middleware.
26
+ */
27
+ export type Changes<S> = { [K in keyof S]: {
28
+ field: K;
29
+ before: S[K];
30
+ after: S[K];
31
+ }; };
32
+ /**
33
+ * A write to one field of the container. With no `K`, the union over every
34
+ * field; `field` discriminates.
35
+ */
36
+ export type Change<S, K extends keyof S = keyof S> = Changes<S>[K];
37
+ /**
38
+ * The change builder of each field. A lookup by a generic key yields the
39
+ * change object of that field, which an object literal cannot express
40
+ * without a cast.
41
+ */
42
+ export type Changers<S> = { [K in keyof S]: (field: K, before: S[K], after: S[K]) => Changes<S>[K]; };
43
+ /**
44
+ * The one event every state container emits: `change`, with the change
45
+ * object of the written field. A service's event map extends this.
46
+ */
47
+ export type ChangeEvents<S> = {
48
+ change: [change: Change<S>];
49
+ };
50
+ /**
51
+ * An event map: each key is an event name, each value the argument list of
52
+ * its listeners.
53
+ */
54
+ export type EventMap = Record<string, unknown[]>;
55
+ /**
56
+ * A typed event bus over an event map.
57
+ */
58
+ export type Emitter<M extends EventMap> = {
59
+ /**
60
+ * Subscribes a listener to an event. Returns the function that removes it.
61
+ */
62
+ on: <E extends keyof M>(event: E, listener: (...args: M[E]) => void) => () => void;
63
+ /**
64
+ * Calls the listeners of an event, in subscription order, over a copy, so
65
+ * a listener can unsubscribe during the call.
66
+ */
67
+ emit: <E extends keyof M>(event: E, ...args: M[E]) => void;
68
+ /**
69
+ * Whether an event has a listener. Skips the work of an event payload when
70
+ * nothing listens.
71
+ */
72
+ heard: (event: keyof M) => boolean;
73
+ };
74
+ /**
75
+ * A listener of one event of an event map.
76
+ */
77
+ export type Listener<M extends EventMap, E extends keyof M> = (...args: M[E]) => void;
78
+ export type Listeners<M extends EventMap> = { [E in keyof M]?: Set<(...args: M[E]) => void>; };
79
+ /**
80
+ * Makes a typed event bus. Listener sets are created on first use.
81
+ */
82
+ export declare const makeEmitter: <M extends EventMap>() => Emitter<M>;
83
+ /**
84
+ * Makes the active state over a container: an `S`-shaped proxy. A read of
85
+ * a field passes the stored value through the matching `options.get`
86
+ * middleware. A write passes the value through the matching `options.set`
87
+ * middleware, stores the result in the container, and emits `change` with
88
+ * one change object. A key outside `fields` goes straight to the container.
89
+ */
90
+ export declare const makeProxy: <S extends object>(state: S, fields: Fields<S>, options: Options<S>, emitter: Emitter<ChangeEvents<S>>) => S;
91
+ /**
92
+ * Builds the change object of a field.
93
+ */
94
+ export declare const change: <S, K extends keyof S>(field: K, before: S[K], after: S[K]) => Changes<S>[K];
@@ -0,0 +1,47 @@
1
+ import { keys, rekey, within } from "objectively";
2
+ const makeEmitter = () => {
3
+ const listeners = {};
4
+ const of = (event) => listeners[event] ??= /* @__PURE__ */ new Set();
5
+ return {
6
+ on: (event, listener) => {
7
+ of(event).add(listener);
8
+ return () => {
9
+ of(event).delete(listener);
10
+ };
11
+ },
12
+ emit: (event, ...args) => {
13
+ for (const listener of Array.from(of(event))) listener(...args);
14
+ },
15
+ heard: (event) => of(event).size > 0
16
+ };
17
+ };
18
+ const change = (field, before, after) => ({
19
+ field,
20
+ before,
21
+ after
22
+ });
23
+ const makeProxy = (state, fields, options, emitter) => {
24
+ const names = keys(fields);
25
+ const changers = rekey(fields, (field) => [field, change]);
26
+ const read = (key) => {
27
+ const through = options.get?.state?.[key];
28
+ const value = state[key];
29
+ return through ? through(value) : value;
30
+ };
31
+ const write = (key, value) => {
32
+ const through = options.set?.state?.[key];
33
+ const before = state[key];
34
+ const after = through ? through(value) : value;
35
+ state[key] = after;
36
+ if (emitter.heard("change")) emitter.emit("change", changers[key](key, before, after));
37
+ };
38
+ return new Proxy(state, {
39
+ get: (target, key) => within(names, key) ? read(key) : Reflect.get(target, key),
40
+ set: (target, key, value) => {
41
+ if (!within(names, key)) return Reflect.set(target, key, value);
42
+ write(key, value);
43
+ return true;
44
+ }
45
+ });
46
+ };
47
+ export { change, makeEmitter, makeProxy };
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Alexander Thorwaldson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,46 @@
1
- # Temporary Holding Version
1
+ # @zoobzio/kit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The mechanisms the headless services share. Pure TypeScript, no framework
4
+ dependency.
5
+
6
+ - `makeEmitter<M>()`: a typed event bus over an event map. `on` subscribes and
7
+ returns the unsubscribe, `emit` calls listeners in subscription order over a
8
+ copy, `heard` reports whether an event has a listener.
9
+ - `makeProxy(state, fields, options, emitter)`: the active state over a
10
+ container. A read of a field passes through `options.get.state.<field>`, a
11
+ write through `options.set.state.<field>`, then lands in the container and
12
+ emits `change` with `{ field, before, after }`. A key outside `fields` goes
13
+ straight to the container.
14
+ - `change(field, before, after)`: builds one change object.
15
+ - The types the above are described in: `Middleware<S>`, `Options<S>`,
16
+ `Fields<S>`, `Changes<S>`, `Change<S, K>`, `Changers<S>`, `ChangeEvents<S>`,
17
+ `EventMap`, `Emitter<M>`, `Listener<M, E>`, `Listeners<M>`.
18
+
19
+ Everything is generic over the container shape `S`. A service binds them to
20
+ its own `State<T>` and re-exports the bound aliases, so a consumer of
21
+ `@zoobzio/table` never imports from the kit directly.
22
+
23
+ ## Usage
24
+
25
+ ```ts
26
+ import { makeEmitter, makeProxy } from "@zoobzio/kit";
27
+ import type { ChangeEvents, Fields, Options } from "@zoobzio/kit";
28
+
29
+ type State = { page: number; query: string };
30
+
31
+ const FIELDS: Fields<State> = { page: null, query: null };
32
+
33
+ type Events = ChangeEvents<State> & { updated: [event: { id: string }] };
34
+
35
+ const emitter = makeEmitter<Events>();
36
+ const options: Options<State> = {
37
+ set: { state: { query: (query) => query.trim() } },
38
+ };
39
+ const state = makeProxy({ page: 1, query: "" }, FIELDS, options, emitter);
40
+
41
+ emitter.on("change", (change) => {
42
+ if (change.field === "page") track(change.after);
43
+ });
44
+
45
+ state.query = " acme "; // stored as "acme", emits change
46
+ ```
package/package.json CHANGED
@@ -1,6 +1,40 @@
1
1
  {
2
2
  "name": "@zoobzio/kit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.0.0",
4
+ "description": "The mechanisms the headless services share: a typed event emitter, a state container proxy with per-field middleware and change events, and the types that describe them. No framework dependency.",
5
+ "license": "MIT",
6
+ "author": {
7
+ "name": "Alex Thorwaldson",
8
+ "email": "alex@zoobz.io",
9
+ "url": "https://zoobz.io"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "git+https://github.com/zoobzio/foundation.git",
14
+ "directory": "packages/kit"
15
+ },
16
+ "files": [
17
+ ".dist"
18
+ ],
19
+ "type": "module",
20
+ "sideEffects": false,
21
+ "main": "./.dist/index.mjs",
22
+ "types": "./.dist/index.d.mts",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./.dist/index.d.mts",
26
+ "import": "./.dist/index.mjs"
27
+ },
28
+ "./package.json": "./package.json"
29
+ },
30
+ "dependencies": {
31
+ "objectively": "^0.3.0"
32
+ },
33
+ "scripts": {
34
+ "build": "obuild",
35
+ "stub": "obuild --stub",
36
+ "lint": "oxlint",
37
+ "typecheck": "tsc --noEmit",
38
+ "test": "vitest run"
39
+ }
6
40
  }