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.
Files changed (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +454 -0
  3. package/dist/device/index.d.mts +312 -0
  4. package/dist/device/index.d.mts.map +1 -0
  5. package/dist/device/index.mjs +490 -0
  6. package/dist/device/index.mjs.map +1 -0
  7. package/dist/dom/index.d.mts +73 -0
  8. package/dist/dom/index.d.mts.map +1 -0
  9. package/dist/dom/index.mjs +218 -0
  10. package/dist/dom/index.mjs.map +1 -0
  11. package/dist/events/index.d.mts +154 -0
  12. package/dist/events/index.d.mts.map +1 -0
  13. package/dist/events/index.mjs +287 -0
  14. package/dist/events/index.mjs.map +1 -0
  15. package/dist/index.d.mts +10 -0
  16. package/dist/index.mjs +10 -0
  17. package/dist/media/index.d.mts +79 -0
  18. package/dist/media/index.d.mts.map +1 -0
  19. package/dist/media/index.mjs +127 -0
  20. package/dist/media/index.mjs.map +1 -0
  21. package/dist/motion/index.d.mts +112 -0
  22. package/dist/motion/index.d.mts.map +1 -0
  23. package/dist/motion/index.mjs +208 -0
  24. package/dist/motion/index.mjs.map +1 -0
  25. package/dist/net/index.d.mts +199 -0
  26. package/dist/net/index.d.mts.map +1 -0
  27. package/dist/net/index.mjs +329 -0
  28. package/dist/net/index.mjs.map +1 -0
  29. package/dist/observers/index.d.mts +59 -0
  30. package/dist/observers/index.d.mts.map +1 -0
  31. package/dist/observers/index.mjs +156 -0
  32. package/dist/observers/index.mjs.map +1 -0
  33. package/dist/state/index.d.mts +342 -0
  34. package/dist/state/index.d.mts.map +1 -0
  35. package/dist/state/index.mjs +504 -0
  36. package/dist/state/index.mjs.map +1 -0
  37. package/dist/time/index.d.mts +129 -0
  38. package/dist/time/index.d.mts.map +1 -0
  39. package/dist/time/index.mjs +213 -0
  40. package/dist/time/index.mjs.map +1 -0
  41. 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
+ }