@askrjs/askr 0.0.42 → 0.0.43

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 (50) hide show
  1. package/dist/benchmark.js +2 -2
  2. package/dist/boot/index.d.ts.map +1 -1
  3. package/dist/boot/index.js +2 -0
  4. package/dist/boot/index.js.map +1 -1
  5. package/dist/common/route-activity.js +30 -0
  6. package/dist/common/route-activity.js.map +1 -0
  7. package/dist/common/router.d.ts +4 -2
  8. package/dist/common/router.d.ts.map +1 -1
  9. package/dist/data/index.d.ts +21 -2
  10. package/dist/data/index.d.ts.map +1 -1
  11. package/dist/data/index.js +56 -3
  12. package/dist/data/index.js.map +1 -1
  13. package/dist/data/invalidation-listeners.js +15 -0
  14. package/dist/data/invalidation-listeners.js.map +1 -0
  15. package/dist/renderer/dom.js +23 -15
  16. package/dist/renderer/dom.js.map +1 -1
  17. package/dist/renderer/evaluate.js +9 -13
  18. package/dist/renderer/evaluate.js.map +1 -1
  19. package/dist/renderer/for-commit.js +9 -0
  20. package/dist/renderer/for-commit.js.map +1 -1
  21. package/dist/resources/index.d.ts +2 -2
  22. package/dist/resources/index.js +2 -2
  23. package/dist/router/match.js +31 -6
  24. package/dist/router/match.js.map +1 -1
  25. package/dist/router/navigate.d.ts.map +1 -1
  26. package/dist/router/navigate.js +30 -4
  27. package/dist/router/navigate.js.map +1 -1
  28. package/dist/router/route.d.ts +6 -1
  29. package/dist/router/route.d.ts.map +1 -1
  30. package/dist/router/route.js +35 -8
  31. package/dist/router/route.js.map +1 -1
  32. package/dist/runtime/component.d.ts +7 -1
  33. package/dist/runtime/component.d.ts.map +1 -1
  34. package/dist/runtime/component.js +205 -44
  35. package/dist/runtime/component.js.map +1 -1
  36. package/dist/runtime/fastlane.js +6 -0
  37. package/dist/runtime/fastlane.js.map +1 -1
  38. package/dist/runtime/operations.d.ts +11 -3
  39. package/dist/runtime/operations.d.ts.map +1 -1
  40. package/dist/runtime/operations.js +161 -22
  41. package/dist/runtime/operations.js.map +1 -1
  42. package/dist/runtime/readable.d.ts +1 -0
  43. package/dist/runtime/readable.d.ts.map +1 -1
  44. package/dist/runtime/readable.js +7 -4
  45. package/dist/runtime/readable.js.map +1 -1
  46. package/dist/testing/index.d.ts +53 -0
  47. package/dist/testing/index.d.ts.map +1 -0
  48. package/dist/testing/index.js +172 -0
  49. package/dist/testing/index.js.map +1 -0
  50. package/package.json +11 -3
@@ -0,0 +1,172 @@
1
+ import { parseSegments } from "../router/match.js";
2
+ import { computeRouteActivityMatches } from "../router/route.js";
3
+ import { addInvalidationListener } from "../data/invalidation-listeners.js";
4
+ //#region src/testing/index.ts
5
+ function normalizeRefresh(options) {
6
+ return async () => {
7
+ await options?.refresh?.();
8
+ };
9
+ }
10
+ function makeQuery(state, options) {
11
+ return {
12
+ ...state,
13
+ refresh: normalizeRefresh(options)
14
+ };
15
+ }
16
+ function createFreshQuery(data, options) {
17
+ return makeQuery({
18
+ data,
19
+ error: null,
20
+ loading: false,
21
+ refreshing: false,
22
+ stale: false,
23
+ consistency: "fresh",
24
+ staleReason: null
25
+ }, options);
26
+ }
27
+ const mockQuery = Object.assign(createFreshQuery, {
28
+ loading(options) {
29
+ return makeQuery({
30
+ data: null,
31
+ error: null,
32
+ loading: true,
33
+ refreshing: false,
34
+ stale: false,
35
+ consistency: "fresh",
36
+ staleReason: null
37
+ }, options);
38
+ },
39
+ error(error, previousData, options) {
40
+ return makeQuery({
41
+ data: previousData ?? null,
42
+ error,
43
+ loading: false,
44
+ refreshing: false,
45
+ stale: true,
46
+ consistency: "stale",
47
+ staleReason: "error"
48
+ }, options);
49
+ },
50
+ refreshing(data, options) {
51
+ return makeQuery({
52
+ data,
53
+ error: null,
54
+ loading: false,
55
+ refreshing: true,
56
+ stale: true,
57
+ consistency: "refreshing",
58
+ staleReason: null
59
+ }, options);
60
+ },
61
+ stale(data, reason = "inconsistent", options) {
62
+ return makeQuery({
63
+ data,
64
+ error: null,
65
+ loading: false,
66
+ refreshing: false,
67
+ stale: true,
68
+ consistency: "stale",
69
+ staleReason: reason
70
+ }, options);
71
+ },
72
+ pendingWrite(data, options) {
73
+ return makeQuery({
74
+ data,
75
+ error: null,
76
+ loading: false,
77
+ refreshing: true,
78
+ stale: true,
79
+ consistency: "pending-write",
80
+ staleReason: null
81
+ }, options);
82
+ }
83
+ });
84
+ const queryState = {
85
+ fresh: createFreshQuery,
86
+ loading: mockQuery.loading,
87
+ error: mockQuery.error,
88
+ refreshing: mockQuery.refreshing,
89
+ stale: mockQuery.stale,
90
+ pendingWrite: mockQuery.pendingWrite
91
+ };
92
+ function createInvalidationRecorder() {
93
+ const records = [];
94
+ let active = true;
95
+ const unsubscribe = addInvalidationListener((event) => {
96
+ records.push({
97
+ prefix: event.prefix,
98
+ markPendingWrite: event.markPendingWrite
99
+ });
100
+ });
101
+ return {
102
+ get calls() {
103
+ return records.slice();
104
+ },
105
+ get prefixes() {
106
+ return records.map((record) => record.prefix);
107
+ },
108
+ clear() {
109
+ records.length = 0;
110
+ },
111
+ stop() {
112
+ if (!active) return;
113
+ active = false;
114
+ unsubscribe();
115
+ }
116
+ };
117
+ }
118
+ function matchRoute(path, options = {}) {
119
+ return computeRouteActivityMatches(path, options)[0] ?? null;
120
+ }
121
+ function getRoutePatternRecords(options) {
122
+ if (options.manifest) return options.manifest.records.map((record) => ({
123
+ path: record.path,
124
+ segments: record.segments,
125
+ namespace: record.options.namespace
126
+ }));
127
+ return (options.routes ?? []).map((route) => ({
128
+ path: route.path,
129
+ segments: parseSegments(route.path),
130
+ namespace: route.namespace
131
+ }));
132
+ }
133
+ function routePrefixMatches(splatPrefix, routeSegments) {
134
+ if (routeSegments.length <= splatPrefix.length) return false;
135
+ for (let index = 0; index < splatPrefix.length; index++) {
136
+ const splatSegment = splatPrefix[index];
137
+ const routeSegment = routeSegments[index];
138
+ if (splatSegment.kind === "static" && routeSegment.kind === "static" && splatSegment.value !== routeSegment.value) return false;
139
+ }
140
+ return true;
141
+ }
142
+ function getRouteWarnings(options = {}) {
143
+ const records = getRoutePatternRecords(options);
144
+ const warnings = [];
145
+ for (const record of records) {
146
+ const splatIndex = record.segments.findIndex((segment) => segment.kind === "splat");
147
+ if (splatIndex === -1) continue;
148
+ const splatPrefix = record.segments.slice(0, splatIndex);
149
+ for (const candidate of records) {
150
+ if (candidate === record || candidate.namespace !== record.namespace || !routePrefixMatches(splatPrefix, candidate.segments)) continue;
151
+ const reservedSegment = candidate.segments[splatIndex];
152
+ if (reservedSegment?.kind !== "static") continue;
153
+ warnings.push({
154
+ kind: "route-collision",
155
+ path: record.path,
156
+ conflictingPath: candidate.path,
157
+ segment: reservedSegment.value,
158
+ namespace: record.namespace,
159
+ message: `Route "${candidate.path}" reserves segment "${reservedSegment.value}" under named splat route "${record.path}".`
160
+ });
161
+ }
162
+ }
163
+ return warnings.sort((left, right) => {
164
+ const pathOrder = left.path.localeCompare(right.path);
165
+ if (pathOrder !== 0) return pathOrder;
166
+ return left.conflictingPath.localeCompare(right.conflictingPath);
167
+ });
168
+ }
169
+ //#endregion
170
+ export { createInvalidationRecorder, getRouteWarnings, matchRoute, mockQuery, queryState };
171
+
172
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/testing/index.ts"],"sourcesContent":["import type { Query, QueryStaleReason } from '../data';\nimport { parseSegments } from '../router/match';\nimport { computeRouteActivityMatches } from '../router/route';\nimport type {\n ParsedSegment,\n Route,\n RouteManifest,\n RouteMatch,\n RouteRecord,\n} from '../common/router';\nimport {\n addInvalidationListener,\n type InvalidationEvent,\n} from '../data/invalidation-listeners';\n\nexport type MockRefresh = () => void | Promise<void>;\n\nexport interface MockQueryOptions {\n refresh?: MockRefresh;\n}\n\nexport interface InvalidationRecord {\n prefix: string;\n markPendingWrite: boolean;\n}\n\nexport interface InvalidationRecorder {\n readonly calls: readonly InvalidationRecord[];\n readonly prefixes: readonly string[];\n clear(): void;\n stop(): void;\n}\n\ninterface MatchRouteOptions {\n manifest?: RouteManifest;\n routes?: readonly Route[];\n}\n\nexport interface RoutePatternWarning {\n kind: 'route-collision';\n path: string;\n conflictingPath: string;\n segment: string;\n namespace: string | undefined;\n message: string;\n}\n\ntype StaleValueReason = Exclude<QueryStaleReason, 'error'>;\n\nfunction normalizeRefresh(options?: MockQueryOptions): () => Promise<void> {\n return async () => {\n await options?.refresh?.();\n };\n}\n\nfunction makeQuery<T extends {}>(\n state: Omit<Query<T>, 'refresh'>,\n options?: MockQueryOptions\n): Query<T> {\n return {\n ...state,\n refresh: normalizeRefresh(options),\n } as Query<T>;\n}\n\nfunction createFreshQuery<T extends {}>(\n data: T,\n options?: MockQueryOptions\n): Query<T> {\n return makeQuery(\n {\n data,\n error: null,\n loading: false,\n refreshing: false,\n stale: false,\n consistency: 'fresh',\n staleReason: null,\n },\n options\n );\n}\n\nexport const mockQuery = Object.assign(createFreshQuery, {\n loading<T extends {} = {}>(options?: MockQueryOptions): Query<T> {\n return makeQuery(\n {\n data: null,\n error: null,\n loading: true,\n refreshing: false,\n stale: false,\n consistency: 'fresh',\n staleReason: null,\n },\n options\n );\n },\n\n error<T extends {} = {}>(\n error: {},\n previousData?: T,\n options?: MockQueryOptions\n ): Query<T> {\n return makeQuery(\n {\n data: previousData ?? null,\n error,\n loading: false,\n refreshing: false,\n stale: true,\n consistency: 'stale',\n staleReason: 'error',\n } as Omit<Query<T>, 'refresh'>,\n options\n );\n },\n\n refreshing<T extends {}>(data: T, options?: MockQueryOptions): Query<T> {\n return makeQuery(\n {\n data,\n error: null,\n loading: false,\n refreshing: true,\n stale: true,\n consistency: 'refreshing',\n staleReason: null,\n },\n options\n );\n },\n\n stale<T extends {}>(\n data: T,\n reason: StaleValueReason = 'inconsistent',\n options?: MockQueryOptions\n ): Query<T> {\n return makeQuery(\n {\n data,\n error: null,\n loading: false,\n refreshing: false,\n stale: true,\n consistency: 'stale',\n staleReason: reason,\n },\n options\n );\n },\n\n pendingWrite<T extends {}>(data: T, options?: MockQueryOptions): Query<T> {\n return makeQuery(\n {\n data,\n error: null,\n loading: false,\n refreshing: true,\n stale: true,\n consistency: 'pending-write',\n staleReason: null,\n },\n options\n );\n },\n});\n\nexport const queryState = {\n fresh: createFreshQuery,\n loading: mockQuery.loading,\n error: mockQuery.error,\n refreshing: mockQuery.refreshing,\n stale: mockQuery.stale,\n pendingWrite: mockQuery.pendingWrite,\n};\n\nexport function createInvalidationRecorder(): InvalidationRecorder {\n const records: InvalidationRecord[] = [];\n let active = true;\n\n const unsubscribe = addInvalidationListener((event: InvalidationEvent) => {\n records.push({\n prefix: event.prefix,\n markPendingWrite: event.markPendingWrite,\n });\n });\n\n return {\n get calls() {\n return records.slice();\n },\n\n get prefixes() {\n return records.map((record) => record.prefix);\n },\n\n clear() {\n records.length = 0;\n },\n\n stop() {\n if (!active) {\n return;\n }\n\n active = false;\n unsubscribe();\n },\n };\n}\n\nexport function matchRoute(\n path: string,\n options: MatchRouteOptions = {}\n): RouteMatch | null {\n return computeRouteActivityMatches(path, options)[0] ?? null;\n}\n\ntype RoutePatternRecord = {\n path: string;\n segments: ParsedSegment[];\n namespace: string | undefined;\n};\n\nfunction getRoutePatternRecords(\n options: MatchRouteOptions\n): RoutePatternRecord[] {\n if (options.manifest) {\n return options.manifest.records.map((record: RouteRecord) => ({\n path: record.path,\n segments: record.segments,\n namespace: record.options.namespace,\n }));\n }\n\n return (options.routes ?? []).map((route) => ({\n path: route.path,\n segments: parseSegments(route.path),\n namespace: route.namespace,\n }));\n}\n\nfunction routePrefixMatches(\n splatPrefix: readonly ParsedSegment[],\n routeSegments: readonly ParsedSegment[]\n): boolean {\n if (routeSegments.length <= splatPrefix.length) {\n return false;\n }\n\n for (let index = 0; index < splatPrefix.length; index++) {\n const splatSegment = splatPrefix[index];\n const routeSegment = routeSegments[index];\n\n if (\n splatSegment.kind === 'static' &&\n routeSegment.kind === 'static' &&\n splatSegment.value !== routeSegment.value\n ) {\n return false;\n }\n }\n\n return true;\n}\n\nexport function getRouteWarnings(\n options: MatchRouteOptions = {}\n): RoutePatternWarning[] {\n const records = getRoutePatternRecords(options);\n const warnings: RoutePatternWarning[] = [];\n\n for (const record of records) {\n const splatIndex = record.segments.findIndex(\n (segment) => segment.kind === 'splat'\n );\n if (splatIndex === -1) {\n continue;\n }\n\n const splatPrefix = record.segments.slice(0, splatIndex);\n for (const candidate of records) {\n if (\n candidate === record ||\n candidate.namespace !== record.namespace ||\n !routePrefixMatches(splatPrefix, candidate.segments)\n ) {\n continue;\n }\n\n const reservedSegment = candidate.segments[splatIndex];\n if (reservedSegment?.kind !== 'static') {\n continue;\n }\n\n warnings.push({\n kind: 'route-collision',\n path: record.path,\n conflictingPath: candidate.path,\n segment: reservedSegment.value,\n namespace: record.namespace,\n message: `Route \"${candidate.path}\" reserves segment \"${reservedSegment.value}\" under named splat route \"${record.path}\".`,\n });\n }\n }\n\n return warnings.sort((left, right) => {\n const pathOrder = left.path.localeCompare(right.path);\n if (pathOrder !== 0) {\n return pathOrder;\n }\n return left.conflictingPath.localeCompare(right.conflictingPath);\n });\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@askrjs/askr",
3
- "version": "0.0.42",
3
+ "version": "0.0.43",
4
4
  "description": "Actor-backed deterministic UI framework",
5
5
  "keywords": [
6
6
  "askr",
@@ -80,6 +80,10 @@
80
80
  "import": "./dist/data/index.js",
81
81
  "types": "./dist/data/index.d.ts"
82
82
  },
83
+ "./testing": {
84
+ "import": "./dist/testing/index.js",
85
+ "types": "./dist/testing/index.d.ts"
86
+ },
83
87
  "./fx": {
84
88
  "import": "./dist/fx/index.js",
85
89
  "types": "./dist/fx/index.d.ts"
@@ -127,7 +131,7 @@
127
131
  "bench:tier4": "cross-env NODE_ENV=production vp test bench --run --reporter=default --config vitest.bench.tier4.config.ts"
128
132
  },
129
133
  "devDependencies": {
130
- "@types/node": "^25.9.2",
134
+ "@types/node": "^25.9.3",
131
135
  "cross-env": "^10.1.0",
132
136
  "jsdom": "^29.1.1",
133
137
  "playwright": "^1.60.0",
@@ -145,5 +149,9 @@
145
149
  "engines": {
146
150
  "node": ">=18"
147
151
  },
148
- "packageManager": "npm@11.16.0"
152
+ "packageManager": "npm@11.17.0",
153
+ "allowScripts": {
154
+ "fsevents@2.3.3": true,
155
+ "fsevents@2.3.2": true
156
+ }
149
157
  }