foldkit-primitives 0.1.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.
- package/LICENSE +21 -0
- package/README.md +454 -0
- package/dist/device/index.d.mts +312 -0
- package/dist/device/index.d.mts.map +1 -0
- package/dist/device/index.mjs +490 -0
- package/dist/device/index.mjs.map +1 -0
- package/dist/dom/index.d.mts +73 -0
- package/dist/dom/index.d.mts.map +1 -0
- package/dist/dom/index.mjs +218 -0
- package/dist/dom/index.mjs.map +1 -0
- package/dist/events/index.d.mts +154 -0
- package/dist/events/index.d.mts.map +1 -0
- package/dist/events/index.mjs +287 -0
- package/dist/events/index.mjs.map +1 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.mjs +10 -0
- package/dist/media/index.d.mts +79 -0
- package/dist/media/index.d.mts.map +1 -0
- package/dist/media/index.mjs +127 -0
- package/dist/media/index.mjs.map +1 -0
- package/dist/motion/index.d.mts +112 -0
- package/dist/motion/index.d.mts.map +1 -0
- package/dist/motion/index.mjs +208 -0
- package/dist/motion/index.mjs.map +1 -0
- package/dist/net/index.d.mts +199 -0
- package/dist/net/index.d.mts.map +1 -0
- package/dist/net/index.mjs +329 -0
- package/dist/net/index.mjs.map +1 -0
- package/dist/observers/index.d.mts +59 -0
- package/dist/observers/index.d.mts.map +1 -0
- package/dist/observers/index.mjs +156 -0
- package/dist/observers/index.mjs.map +1 -0
- package/dist/state/index.d.mts +342 -0
- package/dist/state/index.d.mts.map +1 -0
- package/dist/state/index.mjs +504 -0
- package/dist/state/index.mjs.map +1 -0
- package/dist/time/index.d.mts +129 -0
- package/dist/time/index.d.mts.map +1 -0
- package/dist/time/index.mjs +213 -0
- package/dist/time/index.mjs.map +1 -0
- package/package.json +96 -0
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { Bundle } from "foldkit-bundle";
|
|
3
|
+
//#region src/time/debounce.d.ts
|
|
4
|
+
export declare const debounce: <const Name extends string, Value>(config: {
|
|
5
|
+
readonly name: Name;
|
|
6
|
+
readonly value: Schema.Codec<Value, unknown>;
|
|
7
|
+
}) => Bundle.Bundle<Name, {
|
|
8
|
+
readonly delayMs: number;
|
|
9
|
+
}, {
|
|
10
|
+
readonly latest: Value | null;
|
|
11
|
+
readonly generation: number;
|
|
12
|
+
}, {
|
|
13
|
+
readonly _tag: "Changed";
|
|
14
|
+
readonly value: Value;
|
|
15
|
+
} | {
|
|
16
|
+
readonly _tag: "Settled";
|
|
17
|
+
readonly value: Value;
|
|
18
|
+
readonly generation: number;
|
|
19
|
+
}, {
|
|
20
|
+
readonly _tag: "Debounced";
|
|
21
|
+
readonly value: Value;
|
|
22
|
+
}, never, never, void, {}, {}> & {
|
|
23
|
+
Message: import("foldkit/message").MessageUnion<{
|
|
24
|
+
readonly Changed: {
|
|
25
|
+
readonly value: Schema.Codec<Value, unknown, never, never>;
|
|
26
|
+
};
|
|
27
|
+
readonly Settled: {
|
|
28
|
+
readonly value: Schema.Codec<Value, unknown, never, never>;
|
|
29
|
+
readonly generation: Schema.Number;
|
|
30
|
+
};
|
|
31
|
+
}>;
|
|
32
|
+
Debounced: Schema.TaggedStruct<"Debounced", {
|
|
33
|
+
readonly value: Schema.Codec<Value, unknown, never, never>;
|
|
34
|
+
}>;
|
|
35
|
+
};
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/time/interval.d.ts
|
|
38
|
+
export declare const IntervalModel: Schema.Struct<{
|
|
39
|
+
readonly running: Schema.Boolean;
|
|
40
|
+
readonly lastAt: Schema.NullOr<Schema.Number>;
|
|
41
|
+
}>;
|
|
42
|
+
export type IntervalModel = typeof IntervalModel.Type;
|
|
43
|
+
export declare const IntervalMessage: import("foldkit/message").MessageUnion<{
|
|
44
|
+
readonly Started: {};
|
|
45
|
+
readonly Stopped: {};
|
|
46
|
+
readonly Ticked: {
|
|
47
|
+
readonly at: Schema.Number;
|
|
48
|
+
};
|
|
49
|
+
}>;
|
|
50
|
+
export type IntervalMessage = typeof IntervalMessage.Type;
|
|
51
|
+
export declare const Interval: Bundle.Bundle<"Interval", {
|
|
52
|
+
readonly intervalMs: number;
|
|
53
|
+
}, {
|
|
54
|
+
readonly running: boolean;
|
|
55
|
+
readonly lastAt: number | null;
|
|
56
|
+
}, {
|
|
57
|
+
readonly _tag: "Started";
|
|
58
|
+
} | {
|
|
59
|
+
readonly _tag: "Stopped";
|
|
60
|
+
} | {
|
|
61
|
+
readonly _tag: "Ticked";
|
|
62
|
+
readonly at: number;
|
|
63
|
+
}, never, never, never, void, {}, {}>;
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/time/relative.d.ts
|
|
66
|
+
/**
|
|
67
|
+
* Relative time as a pure function: picks the unit (seconds through years)
|
|
68
|
+
* and delegates the wording to `Intl.RelativeTimeFormat`, so locales come
|
|
69
|
+
* from the platform, not a phrase table. `from` is the earlier moment;
|
|
70
|
+
* swap the arguments for future phrasing ("in 3 days").
|
|
71
|
+
*/
|
|
72
|
+
export declare const formatRelativeTime: (from: Date, to: Date, locale?: string) => string;
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/time/throttle.d.ts
|
|
75
|
+
export declare const ThrottleModel: Schema.Struct<{
|
|
76
|
+
readonly lastAt: Schema.NullOr<Schema.Number>;
|
|
77
|
+
}>;
|
|
78
|
+
export type ThrottleModel = typeof ThrottleModel.Type;
|
|
79
|
+
export declare const ThrottleMessage: import("foldkit/message").MessageUnion<{
|
|
80
|
+
readonly Attempted: {};
|
|
81
|
+
readonly Evaluated: {
|
|
82
|
+
readonly now: Schema.Number;
|
|
83
|
+
};
|
|
84
|
+
}>;
|
|
85
|
+
export type ThrottleMessage = typeof ThrottleMessage.Type;
|
|
86
|
+
export declare const Throttled: Schema.TaggedStruct<"Throttled", {
|
|
87
|
+
readonly at: Schema.Number;
|
|
88
|
+
}>;
|
|
89
|
+
export type Throttled = typeof Throttled.Type;
|
|
90
|
+
export declare const Throttle: Bundle.Bundle<"Throttle", {
|
|
91
|
+
readonly intervalMs: number;
|
|
92
|
+
}, {
|
|
93
|
+
readonly lastAt: number | null;
|
|
94
|
+
}, {
|
|
95
|
+
readonly _tag: "Attempted";
|
|
96
|
+
} | {
|
|
97
|
+
readonly _tag: "Evaluated";
|
|
98
|
+
readonly now: number;
|
|
99
|
+
}, {
|
|
100
|
+
readonly _tag: "Throttled";
|
|
101
|
+
readonly at: number;
|
|
102
|
+
}, never, never, void, {}, {}>;
|
|
103
|
+
//#endregion
|
|
104
|
+
//#region src/time/timer.d.ts
|
|
105
|
+
export declare const TimerModel: Schema.Struct<{
|
|
106
|
+
readonly count: Schema.Number;
|
|
107
|
+
readonly running: Schema.Boolean;
|
|
108
|
+
}>;
|
|
109
|
+
export type TimerModel = typeof TimerModel.Type;
|
|
110
|
+
export declare const TimerMessage: import("foldkit/message").MessageUnion<{
|
|
111
|
+
readonly Started: {};
|
|
112
|
+
readonly Stopped: {};
|
|
113
|
+
readonly Ticked: {};
|
|
114
|
+
}>;
|
|
115
|
+
export type TimerMessage = typeof TimerMessage.Type;
|
|
116
|
+
export declare const Timer: Bundle.Bundle<"Timer", {
|
|
117
|
+
readonly intervalMs: number;
|
|
118
|
+
}, {
|
|
119
|
+
count: number;
|
|
120
|
+
running: boolean;
|
|
121
|
+
}, {
|
|
122
|
+
readonly _tag: "Started";
|
|
123
|
+
} | {
|
|
124
|
+
readonly _tag: "Stopped";
|
|
125
|
+
} | {
|
|
126
|
+
readonly _tag: "Ticked";
|
|
127
|
+
}, never, never, never, void, {}, {}>;
|
|
128
|
+
//#endregion
|
|
129
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../../src/time/debounce.ts","../../src/time/interval.ts","../../src/time/relative.ts","../../src/time/throttle.ts","../../src/time/timer.ts"],"mappings":";;;qBAaa,iBAAkB,qBAAqB,OAAK;WAC9C,MAAM;WACN,OAAO,OAAO,MAAM;MAC9B,OAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;qBCJY,eAAa,OAAA;;;;YAId,uBAAuB,cAAc;qBAEpC,2CAAe;;;;iBAAf,OAAA;;;YAKD,yBAAyB,gBAAgB;qBAExC,UAAQ,OAAA;;;;;;;;;;;;;;;;;;;;;qBCnBR,qBAAkB,MAAU,MAAI,IAAM,MAAI;;;qBCM1C,eAAa,OAAA;;;YACd,uBAAuB,cAAc;qBAEpC,2CAAe;;;kBAAf,OAAA;;;YAID,yBAAyB,gBAAgB;qBAExC,WAAS,OAAA;;;YACV,mBAAmB,UAAU;qBAE5B,UAAQ,OAAA;;;;;;;;;;;;;;;qBCdR,YAAU,OAAA;;;;YACX,oBAAoB,WAAW;qBAE9B,wCAAY;;;;;YAKb,sBAAsB,aAAa;qBAElC,OAAK,OAAA"}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { Clock, Effect, Schema, Stream } from "effect";
|
|
2
|
+
import { defineMessageUnion } from "foldkit/message";
|
|
3
|
+
import * as Subscription from "foldkit/subscription";
|
|
4
|
+
import { Bundle } from "foldkit-bundle";
|
|
5
|
+
//#region src/time/debounce.ts
|
|
6
|
+
/**
|
|
7
|
+
* Debounced input as a bundle factory: `Changed` restarts a timer, and the
|
|
8
|
+
* latest value settles as an OutMessage the placement must handle with
|
|
9
|
+
* `onOut`. Each `Changed` bumps a generation carried by the `Settled` fact
|
|
10
|
+
* it schedules, so a superseded timer's fact emits nothing — the same guard
|
|
11
|
+
* as Presence's exit timeout. Generic over the value Schema, like History.
|
|
12
|
+
* The clock is Effect's, so tests drive it with TestClock.
|
|
13
|
+
*/
|
|
14
|
+
const debounce = (config) => {
|
|
15
|
+
const Model = Schema.Struct({
|
|
16
|
+
latest: Schema.NullOr(config.value),
|
|
17
|
+
generation: Schema.Number
|
|
18
|
+
});
|
|
19
|
+
const Message = defineMessageUnion({
|
|
20
|
+
Changed: { value: config.value },
|
|
21
|
+
Settled: {
|
|
22
|
+
value: config.value,
|
|
23
|
+
generation: Schema.Number
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
const Debounced = Schema.TaggedStruct("Debounced", { value: config.value });
|
|
27
|
+
const bundle = Bundle.make(config.name, {
|
|
28
|
+
Model,
|
|
29
|
+
Message,
|
|
30
|
+
args: Schema.Struct({ delayMs: Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0)), Schema.check(Schema.isFinite())) }),
|
|
31
|
+
init: () => ({ model: {
|
|
32
|
+
latest: null,
|
|
33
|
+
generation: 0
|
|
34
|
+
} }),
|
|
35
|
+
update: (model, message, args) => Message.match(message, {
|
|
36
|
+
Changed: ({ value }) => {
|
|
37
|
+
const generation = model.generation + 1;
|
|
38
|
+
return {
|
|
39
|
+
model: {
|
|
40
|
+
latest: value,
|
|
41
|
+
generation
|
|
42
|
+
},
|
|
43
|
+
commands: [{
|
|
44
|
+
name: `${config.name}.settle`,
|
|
45
|
+
args: { generation },
|
|
46
|
+
effect: Effect.as(Effect.sleep(args.delayMs), Message.Settled({
|
|
47
|
+
value,
|
|
48
|
+
generation
|
|
49
|
+
}))
|
|
50
|
+
}]
|
|
51
|
+
};
|
|
52
|
+
},
|
|
53
|
+
Settled: ({ generation, value }) => generation === model.generation ? {
|
|
54
|
+
model,
|
|
55
|
+
outMessage: Debounced.make({ value })
|
|
56
|
+
} : { model }
|
|
57
|
+
})
|
|
58
|
+
});
|
|
59
|
+
return Object.assign(bundle, {
|
|
60
|
+
Message,
|
|
61
|
+
Debounced
|
|
62
|
+
});
|
|
63
|
+
};
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/time/interval.ts
|
|
66
|
+
/**
|
|
67
|
+
* A wall-clock interval as a bundle: the last tick time in the Model,
|
|
68
|
+
* advanced by a tick stream while running. Sibling of Timer (which counts
|
|
69
|
+
* ticks); this one records *when* they happened, so views can render clocks
|
|
70
|
+
* and elapsed times. The clock is Effect's, so tests drive it with TestClock.
|
|
71
|
+
*/
|
|
72
|
+
const IntervalModel = Schema.Struct({
|
|
73
|
+
running: Schema.Boolean,
|
|
74
|
+
lastAt: Schema.NullOr(Schema.Number)
|
|
75
|
+
});
|
|
76
|
+
const IntervalMessage = defineMessageUnion({
|
|
77
|
+
Started: {},
|
|
78
|
+
Stopped: {},
|
|
79
|
+
Ticked: { at: Schema.Number }
|
|
80
|
+
});
|
|
81
|
+
const Interval = Bundle.make("Interval", {
|
|
82
|
+
Model: IntervalModel,
|
|
83
|
+
Message: IntervalMessage,
|
|
84
|
+
args: Schema.Struct({ intervalMs: Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0)), Schema.check(Schema.isFinite())) }),
|
|
85
|
+
init: () => ({ model: {
|
|
86
|
+
running: false,
|
|
87
|
+
lastAt: null
|
|
88
|
+
} }),
|
|
89
|
+
update: (model, message) => IntervalMessage.match(message, {
|
|
90
|
+
Started: () => ({ model: {
|
|
91
|
+
...model,
|
|
92
|
+
running: true
|
|
93
|
+
} }),
|
|
94
|
+
Stopped: () => ({ model: {
|
|
95
|
+
...model,
|
|
96
|
+
running: false
|
|
97
|
+
} }),
|
|
98
|
+
Ticked: ({ at }) => ({ model: {
|
|
99
|
+
...model,
|
|
100
|
+
lastAt: at
|
|
101
|
+
} })
|
|
102
|
+
}),
|
|
103
|
+
subscriptions: ({ intervalMs }) => Subscription.make()((entry) => ({ ticks: entry({ running: Schema.Boolean }, {
|
|
104
|
+
modelToDependencies: (model) => ({ running: model.running }),
|
|
105
|
+
dependenciesToStream: ({ running }) => running ? Stream.mapEffect(Stream.tick(intervalMs), () => Clock.currentTimeMillis).pipe(Stream.map((at) => IntervalMessage.Ticked({ at }))) : Stream.empty
|
|
106
|
+
}) }))
|
|
107
|
+
});
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/time/relative.ts
|
|
110
|
+
/**
|
|
111
|
+
* Relative time as a pure function: picks the unit (seconds through years)
|
|
112
|
+
* and delegates the wording to `Intl.RelativeTimeFormat`, so locales come
|
|
113
|
+
* from the platform, not a phrase table. `from` is the earlier moment;
|
|
114
|
+
* swap the arguments for future phrasing ("in 3 days").
|
|
115
|
+
*/
|
|
116
|
+
const formatRelativeTime = (from, to, locale = "en") => {
|
|
117
|
+
const format = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
|
|
118
|
+
const seconds = Math.round((to.getTime() - from.getTime()) / 1e3);
|
|
119
|
+
const abs = Math.abs(seconds);
|
|
120
|
+
if (abs < 60) return format.format(-seconds, "second");
|
|
121
|
+
const minutes = Math.round(seconds / 60);
|
|
122
|
+
if (abs < 3600) return format.format(-minutes, "minute");
|
|
123
|
+
const hours = Math.round(seconds / 3600);
|
|
124
|
+
if (abs < 86400) return format.format(-hours, "hour");
|
|
125
|
+
const days = Math.round(seconds / 86400);
|
|
126
|
+
if (abs < 604800) return format.format(-days, "day");
|
|
127
|
+
const weeks = Math.round(seconds / 604800);
|
|
128
|
+
if (abs < 2592e3) return format.format(-weeks, "week");
|
|
129
|
+
const months = Math.round(seconds / 2592e3);
|
|
130
|
+
if (abs < 31536e3) return format.format(-months, "month");
|
|
131
|
+
return format.format(-Math.round(seconds / 31536e3), "year");
|
|
132
|
+
};
|
|
133
|
+
//#endregion
|
|
134
|
+
//#region src/time/throttle.ts
|
|
135
|
+
/**
|
|
136
|
+
* Leading-edge throttle as a bundle: the first `Attempted` in an interval
|
|
137
|
+
* surfaces a `Throttled` OutMessage, the rest are dropped. `Attempted`
|
|
138
|
+
* reads the clock through a Command, so the check runs against Effect time
|
|
139
|
+
* and tests drive it with TestClock. Trailing-edge needs are Debounce's:
|
|
140
|
+
* pair the two rather than adding a second timer here.
|
|
141
|
+
*/
|
|
142
|
+
const ThrottleModel = Schema.Struct({ lastAt: Schema.NullOr(Schema.Number) });
|
|
143
|
+
const ThrottleMessage = defineMessageUnion({
|
|
144
|
+
Attempted: {},
|
|
145
|
+
Evaluated: { now: Schema.Number }
|
|
146
|
+
});
|
|
147
|
+
const Throttled = Schema.TaggedStruct("Throttled", { at: Schema.Number });
|
|
148
|
+
const Throttle = Bundle.make("Throttle", {
|
|
149
|
+
Model: ThrottleModel,
|
|
150
|
+
Message: ThrottleMessage,
|
|
151
|
+
args: Schema.Struct({ intervalMs: Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0)), Schema.check(Schema.isFinite())) }),
|
|
152
|
+
init: () => ({ model: { lastAt: null } }),
|
|
153
|
+
update: (model, message, args) => ThrottleMessage.match(message, {
|
|
154
|
+
Attempted: () => ({
|
|
155
|
+
model,
|
|
156
|
+
commands: [{
|
|
157
|
+
name: "Throttle.evaluate",
|
|
158
|
+
effect: Effect.map(Clock.currentTimeMillis, (now) => ThrottleMessage.Evaluated({ now }))
|
|
159
|
+
}]
|
|
160
|
+
}),
|
|
161
|
+
Evaluated: ({ now }) => model.lastAt === null || now - model.lastAt >= args.intervalMs ? {
|
|
162
|
+
model: { lastAt: now },
|
|
163
|
+
outMessage: Throttled.make({ at: now })
|
|
164
|
+
} : { model }
|
|
165
|
+
})
|
|
166
|
+
});
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region src/time/timer.ts
|
|
169
|
+
/**
|
|
170
|
+
* A timer as a bundle: a count in the Model, advanced by a tick stream while
|
|
171
|
+
* running. The clock is Effect's, so tests drive it with TestClock instead of
|
|
172
|
+
* waiting.
|
|
173
|
+
*/
|
|
174
|
+
const TimerModel = Schema.Struct({
|
|
175
|
+
count: Schema.Number,
|
|
176
|
+
running: Schema.Boolean
|
|
177
|
+
});
|
|
178
|
+
const TimerMessage = defineMessageUnion({
|
|
179
|
+
Started: {},
|
|
180
|
+
Stopped: {},
|
|
181
|
+
Ticked: {}
|
|
182
|
+
});
|
|
183
|
+
const Timer = Bundle.make("Timer", {
|
|
184
|
+
Model: TimerModel,
|
|
185
|
+
Message: TimerMessage,
|
|
186
|
+
args: Schema.Struct({ intervalMs: Schema.Number.pipe(Schema.check(Schema.isGreaterThan(0)), Schema.check(Schema.isFinite())) }),
|
|
187
|
+
init: () => ({ model: {
|
|
188
|
+
count: 0,
|
|
189
|
+
running: false
|
|
190
|
+
} }),
|
|
191
|
+
update: (model, message) => TimerMessage.match(message, {
|
|
192
|
+
Started: () => ({ model: {
|
|
193
|
+
...model,
|
|
194
|
+
running: true
|
|
195
|
+
} }),
|
|
196
|
+
Stopped: () => ({ model: {
|
|
197
|
+
...model,
|
|
198
|
+
running: false
|
|
199
|
+
} }),
|
|
200
|
+
Ticked: () => ({ model: {
|
|
201
|
+
...model,
|
|
202
|
+
count: model.count + 1
|
|
203
|
+
} })
|
|
204
|
+
}),
|
|
205
|
+
subscriptions: ({ intervalMs }) => Subscription.make()((entry) => ({ ticks: entry({ running: Schema.Boolean }, {
|
|
206
|
+
modelToDependencies: (model) => ({ running: model.running }),
|
|
207
|
+
dependenciesToStream: ({ running }) => running ? Stream.map(Stream.tick(intervalMs), () => TimerMessage.Ticked()) : Stream.empty
|
|
208
|
+
}) }))
|
|
209
|
+
});
|
|
210
|
+
//#endregion
|
|
211
|
+
export { Interval, IntervalMessage, IntervalModel, Throttle, ThrottleMessage, ThrottleModel, Throttled, Timer, TimerMessage, TimerModel, debounce, formatRelativeTime };
|
|
212
|
+
|
|
213
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/time/debounce.ts","../../src/time/interval.ts","../../src/time/relative.ts","../../src/time/throttle.ts","../../src/time/timer.ts"],"sourcesContent":["/**\n * Debounced input as a bundle factory: `Changed` restarts a timer, and the\n * latest value settles as an OutMessage the placement must handle with\n * `onOut`. Each `Changed` bumps a generation carried by the `Settled` fact\n * it schedules, so a superseded timer's fact emits nothing — the same guard\n * as Presence's exit timeout. Generic over the value Schema, like History.\n * The clock is Effect's, so tests drive it with TestClock.\n */\nimport { Effect, Schema } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const debounce = <const Name extends string, Value>(config: {\n readonly name: Name\n readonly value: Schema.Codec<Value, unknown>\n}) => {\n const Model = Schema.Struct({\n latest: Schema.NullOr(config.value),\n generation: Schema.Number,\n })\n type Model = typeof Model.Type\n\n const Message = defineMessageUnion({\n Changed: { value: config.value },\n Settled: { value: config.value, generation: Schema.Number },\n })\n type Message = typeof Message.Type\n\n const Debounced = Schema.TaggedStruct('Debounced', { value: config.value })\n type Debounced = typeof Debounced.Type\n\n const bundle = Bundle.make(config.name, {\n Model,\n Message,\n args: Schema.Struct({\n delayMs: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThan(0)),\n Schema.check(Schema.isFinite()),\n ),\n }),\n init: () => ({ model: { latest: null, generation: 0 } }),\n update: (model, message, args) =>\n Message.match<Update.ReturnWithOutMessage<Model, Message, Debounced>>(message, {\n Changed: ({ value }) => {\n const generation = model.generation + 1\n return {\n model: { latest: value, generation },\n commands: [\n {\n name: `${config.name}.settle`,\n args: { generation },\n effect: Effect.as(\n Effect.sleep(args.delayMs),\n Message.Settled({ value, generation }),\n ),\n },\n ],\n }\n },\n // A superseded timer's fact arrives with an old generation: no\n // OutMessage, so the placement never sees it.\n Settled: ({ generation, value }) =>\n generation === model.generation\n ? { model, outMessage: Debounced.make({ value }) }\n : { model },\n }),\n })\n\n return Object.assign(bundle, { Message, Debounced })\n}\n","/**\n * A wall-clock interval as a bundle: the last tick time in the Model,\n * advanced by a tick stream while running. Sibling of Timer (which counts\n * ticks); this one records *when* they happened, so views can render clocks\n * and elapsed times. The clock is Effect's, so tests drive it with TestClock.\n */\nimport { Clock, Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as Subscription from 'foldkit/subscription'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const IntervalModel = Schema.Struct({\n running: Schema.Boolean,\n lastAt: Schema.NullOr(Schema.Number),\n})\nexport type IntervalModel = typeof IntervalModel.Type\n\nexport const IntervalMessage = defineMessageUnion({\n Started: {},\n Stopped: {},\n Ticked: { at: Schema.Number },\n})\nexport type IntervalMessage = typeof IntervalMessage.Type\n\nexport const Interval = Bundle.make('Interval', {\n Model: IntervalModel,\n Message: IntervalMessage,\n args: Schema.Struct({\n intervalMs: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThan(0)),\n Schema.check(Schema.isFinite()),\n ),\n }),\n init: () => ({ model: { running: false, lastAt: null } }),\n update: (model, message) =>\n IntervalMessage.match<Update.ReturnWithOutMessage<IntervalModel, IntervalMessage, never>>(\n message,\n {\n Started: () => ({ model: { ...model, running: true } }),\n Stopped: () => ({ model: { ...model, running: false } }),\n Ticked: ({ at }) => ({ model: { ...model, lastAt: at } }),\n },\n ),\n subscriptions: ({ intervalMs }): Subscription.Subscriptions<IntervalModel, IntervalMessage> =>\n Subscription.make<IntervalModel, IntervalMessage>()(entry => ({\n ticks: entry(\n { running: Schema.Boolean },\n {\n modelToDependencies: model => ({ running: model.running }),\n dependenciesToStream: ({ running }) =>\n running\n ? Stream.mapEffect(Stream.tick(intervalMs), () => Clock.currentTimeMillis).pipe(\n Stream.map(at => IntervalMessage.Ticked({ at })),\n )\n : Stream.empty,\n },\n ),\n })),\n})\n","/**\n * Relative time as a pure function: picks the unit (seconds through years)\n * and delegates the wording to `Intl.RelativeTimeFormat`, so locales come\n * from the platform, not a phrase table. `from` is the earlier moment;\n * swap the arguments for future phrasing (\"in 3 days\").\n */\nexport const formatRelativeTime = (from: Date, to: Date, locale = 'en'): string => {\n const format = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' })\n const seconds = Math.round((to.getTime() - from.getTime()) / 1000)\n const abs = Math.abs(seconds)\n if (abs < 60) return format.format(-seconds, 'second')\n const minutes = Math.round(seconds / 60)\n if (abs < 60 * 60) return format.format(-minutes, 'minute')\n const hours = Math.round(seconds / (60 * 60))\n if (abs < 24 * 60 * 60) return format.format(-hours, 'hour')\n const days = Math.round(seconds / (24 * 60 * 60))\n if (abs < 7 * 24 * 60 * 60) return format.format(-days, 'day')\n const weeks = Math.round(seconds / (7 * 24 * 60 * 60))\n if (abs < 30 * 24 * 60 * 60) return format.format(-weeks, 'week')\n const months = Math.round(seconds / (30 * 24 * 60 * 60))\n if (abs < 365 * 24 * 60 * 60) return format.format(-months, 'month')\n return format.format(-Math.round(seconds / (365 * 24 * 60 * 60)), 'year')\n}\n","/**\n * Leading-edge throttle as a bundle: the first `Attempted` in an interval\n * surfaces a `Throttled` OutMessage, the rest are dropped. `Attempted`\n * reads the clock through a Command, so the check runs against Effect time\n * and tests drive it with TestClock. Trailing-edge needs are Debounce's:\n * pair the two rather than adding a second timer here.\n */\nimport { Clock, Effect, Schema } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport type * as Update from 'foldkit/update'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const ThrottleModel = Schema.Struct({ lastAt: Schema.NullOr(Schema.Number) })\nexport type ThrottleModel = typeof ThrottleModel.Type\n\nexport const ThrottleMessage = defineMessageUnion({\n Attempted: {},\n Evaluated: { now: Schema.Number },\n})\nexport type ThrottleMessage = typeof ThrottleMessage.Type\n\nexport const Throttled = Schema.TaggedStruct('Throttled', { at: Schema.Number })\nexport type Throttled = typeof Throttled.Type\n\nexport const Throttle = Bundle.make('Throttle', {\n Model: ThrottleModel,\n Message: ThrottleMessage,\n args: Schema.Struct({\n intervalMs: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThan(0)),\n Schema.check(Schema.isFinite()),\n ),\n }),\n init: () => ({ model: { lastAt: null } }),\n update: (model, message, args) =>\n ThrottleMessage.match<Update.ReturnWithOutMessage<ThrottleModel, ThrottleMessage, Throttled>>(\n message,\n {\n Attempted: () => ({\n model,\n commands: [\n {\n name: 'Throttle.evaluate',\n effect: Effect.map(Clock.currentTimeMillis, now =>\n ThrottleMessage.Evaluated({ now }),\n ),\n },\n ],\n }),\n // First attempt ever, or the interval has passed: fire and stamp.\n Evaluated: ({ now }) =>\n model.lastAt === null || now - model.lastAt >= args.intervalMs\n ? { model: { lastAt: now }, outMessage: Throttled.make({ at: now }) }\n : { model },\n },\n ),\n})\n","/**\n * A timer as a bundle: a count in the Model, advanced by a tick stream while\n * running. The clock is Effect's, so tests drive it with TestClock instead of\n * waiting.\n */\nimport { Schema, Stream } from 'effect'\nimport { defineMessageUnion } from 'foldkit/message'\nimport * as Subscription from 'foldkit/subscription'\nimport { Bundle } from 'foldkit-bundle'\n\nexport const TimerModel = Schema.Struct({ count: Schema.Number, running: Schema.Boolean })\nexport type TimerModel = typeof TimerModel.Type\n\nexport const TimerMessage = defineMessageUnion({\n Started: {},\n Stopped: {},\n Ticked: {},\n})\nexport type TimerMessage = typeof TimerMessage.Type\n\nexport const Timer = Bundle.make('Timer', {\n Model: TimerModel,\n Message: TimerMessage,\n args: Schema.Struct({\n intervalMs: Schema.Number.pipe(\n Schema.check(Schema.isGreaterThan(0)),\n Schema.check(Schema.isFinite()),\n ),\n }),\n init: () => ({ model: { count: 0, running: false } }),\n update: (model, message) =>\n TimerMessage.match(message, {\n Started: () => ({ model: { ...model, running: true } }),\n Stopped: () => ({ model: { ...model, running: false } }),\n Ticked: () => ({ model: { ...model, count: model.count + 1 } }),\n }),\n subscriptions: ({ intervalMs }): Subscription.Subscriptions<TimerModel, TimerMessage> =>\n Subscription.make<TimerModel, TimerMessage>()(entry => ({\n ticks: entry(\n { running: Schema.Boolean },\n {\n modelToDependencies: model => ({ running: model.running }),\n dependenciesToStream: ({ running }) =>\n running\n ? Stream.map(Stream.tick(intervalMs), () => TimerMessage.Ticked())\n : Stream.empty,\n },\n ),\n })),\n})\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "foldkit-primitives",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Ready-made primitives for the Elm architecture, packaged with foldkit-bundle: media queries, presence, timers, pagination, history, sockets, observers, and clipboard.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"foldkit",
|
|
7
|
+
"effect",
|
|
8
|
+
"submodel",
|
|
9
|
+
"primitives",
|
|
10
|
+
"elm-architecture"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"author": "Patrick Glenn <doeixd@gmail.com>",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/doeixd/foldkit-plus.git",
|
|
21
|
+
"directory": "packages/primitives"
|
|
22
|
+
},
|
|
23
|
+
"homepage": "https://github.com/doeixd/foldkit-plus/tree/main/packages/primitives#readme",
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/doeixd/foldkit-plus/issues"
|
|
26
|
+
},
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.mts",
|
|
31
|
+
"import": "./dist/index.mjs"
|
|
32
|
+
},
|
|
33
|
+
"./dom": {
|
|
34
|
+
"types": "./dist/dom/index.d.mts",
|
|
35
|
+
"import": "./dist/dom/index.mjs"
|
|
36
|
+
},
|
|
37
|
+
"./device": {
|
|
38
|
+
"types": "./dist/device/index.d.mts",
|
|
39
|
+
"import": "./dist/device/index.mjs"
|
|
40
|
+
},
|
|
41
|
+
"./events": {
|
|
42
|
+
"types": "./dist/events/index.d.mts",
|
|
43
|
+
"import": "./dist/events/index.mjs"
|
|
44
|
+
},
|
|
45
|
+
"./media": {
|
|
46
|
+
"types": "./dist/media/index.d.mts",
|
|
47
|
+
"import": "./dist/media/index.mjs"
|
|
48
|
+
},
|
|
49
|
+
"./motion": {
|
|
50
|
+
"types": "./dist/motion/index.d.mts",
|
|
51
|
+
"import": "./dist/motion/index.mjs"
|
|
52
|
+
},
|
|
53
|
+
"./net": {
|
|
54
|
+
"types": "./dist/net/index.d.mts",
|
|
55
|
+
"import": "./dist/net/index.mjs"
|
|
56
|
+
},
|
|
57
|
+
"./observers": {
|
|
58
|
+
"types": "./dist/observers/index.d.mts",
|
|
59
|
+
"import": "./dist/observers/index.mjs"
|
|
60
|
+
},
|
|
61
|
+
"./state": {
|
|
62
|
+
"types": "./dist/state/index.d.mts",
|
|
63
|
+
"import": "./dist/state/index.mjs"
|
|
64
|
+
},
|
|
65
|
+
"./time": {
|
|
66
|
+
"types": "./dist/time/index.d.mts",
|
|
67
|
+
"import": "./dist/time/index.mjs"
|
|
68
|
+
},
|
|
69
|
+
"./package.json": "./package.json"
|
|
70
|
+
},
|
|
71
|
+
"files": [
|
|
72
|
+
"dist",
|
|
73
|
+
"README.md",
|
|
74
|
+
"LICENSE"
|
|
75
|
+
],
|
|
76
|
+
"dependencies": {
|
|
77
|
+
"foldkit-bundle": "0.1.0"
|
|
78
|
+
},
|
|
79
|
+
"peerDependencies": {
|
|
80
|
+
"effect": "^4.0.0-rc.112",
|
|
81
|
+
"foldkit": "^0.158.2"
|
|
82
|
+
},
|
|
83
|
+
"devDependencies": {
|
|
84
|
+
"@types/ws": "^8.5.12",
|
|
85
|
+
"effect": "4.0.0-rc.112",
|
|
86
|
+
"foldkit": "0.158.2",
|
|
87
|
+
"jsdom": "26.1.0",
|
|
88
|
+
"tsdown": "^0.23.0",
|
|
89
|
+
"typescript": "^5.7.2",
|
|
90
|
+
"ws": "^8.18.0"
|
|
91
|
+
},
|
|
92
|
+
"scripts": {
|
|
93
|
+
"build": "tsdown",
|
|
94
|
+
"typecheck": "tsc -b"
|
|
95
|
+
}
|
|
96
|
+
}
|