@cratis/fundamentals 7.16.7 → 7.17.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 (246) hide show
  1. package/ConceptAs.ts +3 -0
  2. package/DateOnly.ts +112 -0
  3. package/Guid.ts +3 -0
  4. package/JsonSerializer.ts +108 -39
  5. package/TimeOnly.ts +108 -0
  6. package/TimeSpan.ts +4 -0
  7. package/ValueMap.ts +3 -0
  8. package/dist/ConceptAs.d.ts +2 -0
  9. package/dist/ConceptAs.d.ts.map +1 -1
  10. package/dist/ConceptAs.js +2 -0
  11. package/dist/ConceptAs.js.map +1 -1
  12. package/dist/DateOnly.d.ts +14 -0
  13. package/dist/DateOnly.d.ts.map +1 -0
  14. package/dist/DateOnly.js +51 -0
  15. package/dist/DateOnly.js.map +1 -0
  16. package/dist/Guid.d.ts +2 -0
  17. package/dist/Guid.d.ts.map +1 -1
  18. package/dist/Guid.js +2 -0
  19. package/dist/Guid.js.map +1 -1
  20. package/dist/JsonSerializer.d.ts +2 -0
  21. package/dist/JsonSerializer.d.ts.map +1 -1
  22. package/dist/JsonSerializer.js +45 -35
  23. package/dist/JsonSerializer.js.map +1 -1
  24. package/dist/TimeOnly.d.ts +13 -0
  25. package/dist/TimeOnly.d.ts.map +1 -0
  26. package/dist/TimeOnly.js +58 -0
  27. package/dist/TimeOnly.js.map +1 -0
  28. package/dist/TimeSpan.d.ts +2 -0
  29. package/dist/TimeSpan.d.ts.map +1 -1
  30. package/dist/TimeSpan.js +2 -0
  31. package/dist/TimeSpan.js.map +1 -1
  32. package/dist/ValueMap.d.ts +2 -0
  33. package/dist/ValueMap.d.ts.map +1 -1
  34. package/dist/ValueMap.js +2 -0
  35. package/dist/ValueMap.js.map +1 -1
  36. package/dist/cjs/ConceptAs.d.ts +2 -0
  37. package/dist/cjs/ConceptAs.d.ts.map +1 -1
  38. package/dist/cjs/ConceptAs.js +3 -0
  39. package/dist/cjs/ConceptAs.js.map +1 -1
  40. package/dist/cjs/DateOnly.d.ts +14 -0
  41. package/dist/cjs/DateOnly.d.ts.map +1 -0
  42. package/dist/cjs/DateOnly.js +56 -0
  43. package/dist/cjs/DateOnly.js.map +1 -0
  44. package/dist/cjs/Guid.d.ts +2 -0
  45. package/dist/cjs/Guid.d.ts.map +1 -1
  46. package/dist/cjs/Guid.js +3 -0
  47. package/dist/cjs/Guid.js.map +1 -1
  48. package/dist/cjs/JsonSerializer.d.ts +2 -0
  49. package/dist/cjs/JsonSerializer.d.ts.map +1 -1
  50. package/dist/cjs/JsonSerializer.js +46 -34
  51. package/dist/cjs/JsonSerializer.js.map +1 -1
  52. package/dist/cjs/TimeOnly.d.ts +13 -0
  53. package/dist/cjs/TimeOnly.d.ts.map +1 -0
  54. package/dist/cjs/TimeOnly.js +63 -0
  55. package/dist/cjs/TimeOnly.js.map +1 -0
  56. package/dist/cjs/TimeSpan.d.ts +2 -0
  57. package/dist/cjs/TimeSpan.d.ts.map +1 -1
  58. package/dist/cjs/TimeSpan.js +2 -0
  59. package/dist/cjs/TimeSpan.js.map +1 -1
  60. package/dist/cjs/ValueMap.d.ts +2 -0
  61. package/dist/cjs/ValueMap.d.ts.map +1 -1
  62. package/dist/cjs/ValueMap.js +3 -0
  63. package/dist/cjs/ValueMap.js.map +1 -1
  64. package/dist/cjs/duplicateInstanceGuard.d.ts +2 -0
  65. package/dist/cjs/duplicateInstanceGuard.d.ts.map +1 -0
  66. package/dist/cjs/duplicateInstanceGuard.js +53 -0
  67. package/dist/cjs/duplicateInstanceGuard.js.map +1 -0
  68. package/dist/cjs/geospatial/LineString.d.ts +2 -0
  69. package/dist/cjs/geospatial/LineString.d.ts.map +1 -1
  70. package/dist/cjs/geospatial/LineString.js +3 -0
  71. package/dist/cjs/geospatial/LineString.js.map +1 -1
  72. package/dist/cjs/geospatial/Point.d.ts +2 -0
  73. package/dist/cjs/geospatial/Point.d.ts.map +1 -1
  74. package/dist/cjs/geospatial/Point.js +2 -0
  75. package/dist/cjs/geospatial/Point.js.map +1 -1
  76. package/dist/cjs/geospatial/Polygon.d.ts +2 -0
  77. package/dist/cjs/geospatial/Polygon.d.ts.map +1 -1
  78. package/dist/cjs/geospatial/Polygon.js +3 -0
  79. package/dist/cjs/geospatial/Polygon.js.map +1 -1
  80. package/dist/cjs/geospatial/index.js +14 -0
  81. package/dist/cjs/geospatial/index.js.map +1 -0
  82. package/dist/cjs/index.d.ts +3 -0
  83. package/dist/cjs/index.d.ts.map +1 -1
  84. package/dist/cjs/index.js +14 -0
  85. package/dist/cjs/index.js.map +1 -1
  86. package/dist/cjs/json/DateOnlyJsonConverter.d.ts +9 -0
  87. package/dist/cjs/json/DateOnlyJsonConverter.d.ts.map +1 -0
  88. package/dist/cjs/json/DateOnlyJsonConverter.js +19 -0
  89. package/dist/cjs/json/DateOnlyJsonConverter.js.map +1 -0
  90. package/dist/cjs/json/JsonConverter.d.ts.map +1 -1
  91. package/dist/cjs/json/JsonConverter.js.map +1 -1
  92. package/dist/cjs/json/TimeOnlyJsonConverter.d.ts +9 -0
  93. package/dist/cjs/json/TimeOnlyJsonConverter.d.ts.map +1 -0
  94. package/dist/cjs/json/TimeOnlyJsonConverter.js +19 -0
  95. package/dist/cjs/json/TimeOnlyJsonConverter.js.map +1 -0
  96. package/dist/cjs/json/index.d.ts +2 -0
  97. package/dist/cjs/json/index.d.ts.map +1 -1
  98. package/dist/cjs/json/index.js +26 -0
  99. package/dist/cjs/json/index.js.map +1 -0
  100. package/dist/cjs/typeKey.d.ts +7 -0
  101. package/dist/cjs/typeKey.d.ts.map +1 -0
  102. package/dist/cjs/typeKey.js +14 -0
  103. package/dist/cjs/typeKey.js.map +1 -0
  104. package/dist/duplicateInstanceGuard.d.ts +2 -0
  105. package/dist/duplicateInstanceGuard.d.ts.map +1 -0
  106. package/dist/duplicateInstanceGuard.js +49 -0
  107. package/dist/duplicateInstanceGuard.js.map +1 -0
  108. package/dist/esm/ConceptAs.d.ts +2 -0
  109. package/dist/esm/ConceptAs.d.ts.map +1 -1
  110. package/dist/esm/ConceptAs.js +3 -0
  111. package/dist/esm/ConceptAs.js.map +1 -1
  112. package/dist/esm/DateOnly.d.ts +14 -0
  113. package/dist/esm/DateOnly.d.ts.map +1 -0
  114. package/dist/esm/DateOnly.js +54 -0
  115. package/dist/esm/DateOnly.js.map +1 -0
  116. package/dist/esm/Guid.d.ts +2 -0
  117. package/dist/esm/Guid.d.ts.map +1 -1
  118. package/dist/esm/Guid.js +3 -0
  119. package/dist/esm/Guid.js.map +1 -1
  120. package/dist/esm/JsonSerializer.d.ts +2 -0
  121. package/dist/esm/JsonSerializer.d.ts.map +1 -1
  122. package/dist/esm/JsonSerializer.js +46 -34
  123. package/dist/esm/JsonSerializer.js.map +1 -1
  124. package/dist/esm/TimeOnly.d.ts +13 -0
  125. package/dist/esm/TimeOnly.d.ts.map +1 -0
  126. package/dist/esm/TimeOnly.js +61 -0
  127. package/dist/esm/TimeOnly.js.map +1 -0
  128. package/dist/esm/TimeSpan.d.ts +2 -0
  129. package/dist/esm/TimeSpan.d.ts.map +1 -1
  130. package/dist/esm/TimeSpan.js +2 -0
  131. package/dist/esm/TimeSpan.js.map +1 -1
  132. package/dist/esm/ValueMap.d.ts +2 -0
  133. package/dist/esm/ValueMap.d.ts.map +1 -1
  134. package/dist/esm/ValueMap.js +3 -0
  135. package/dist/esm/ValueMap.js.map +1 -1
  136. package/dist/esm/duplicateInstanceGuard.d.ts +2 -0
  137. package/dist/esm/duplicateInstanceGuard.d.ts.map +1 -0
  138. package/dist/esm/duplicateInstanceGuard.js +51 -0
  139. package/dist/esm/duplicateInstanceGuard.js.map +1 -0
  140. package/dist/esm/geospatial/LineString.d.ts +2 -0
  141. package/dist/esm/geospatial/LineString.d.ts.map +1 -1
  142. package/dist/esm/geospatial/LineString.js +3 -0
  143. package/dist/esm/geospatial/LineString.js.map +1 -1
  144. package/dist/esm/geospatial/Point.d.ts +2 -0
  145. package/dist/esm/geospatial/Point.d.ts.map +1 -1
  146. package/dist/esm/geospatial/Point.js +2 -0
  147. package/dist/esm/geospatial/Point.js.map +1 -1
  148. package/dist/esm/geospatial/Polygon.d.ts +2 -0
  149. package/dist/esm/geospatial/Polygon.d.ts.map +1 -1
  150. package/dist/esm/geospatial/Polygon.js +3 -0
  151. package/dist/esm/geospatial/Polygon.js.map +1 -1
  152. package/dist/esm/geospatial/index.js +5 -0
  153. package/dist/esm/geospatial/index.js.map +1 -0
  154. package/dist/esm/index.d.ts +3 -0
  155. package/dist/esm/index.d.ts.map +1 -1
  156. package/dist/esm/index.js +5 -0
  157. package/dist/esm/index.js.map +1 -1
  158. package/dist/esm/json/DateOnlyJsonConverter.d.ts +9 -0
  159. package/dist/esm/json/DateOnlyJsonConverter.d.ts.map +1 -0
  160. package/dist/esm/json/DateOnlyJsonConverter.js +17 -0
  161. package/dist/esm/json/DateOnlyJsonConverter.js.map +1 -0
  162. package/dist/esm/json/JsonConverter.d.ts.map +1 -1
  163. package/dist/esm/json/JsonConverter.js.map +1 -1
  164. package/dist/esm/json/TimeOnlyJsonConverter.d.ts +9 -0
  165. package/dist/esm/json/TimeOnlyJsonConverter.d.ts.map +1 -0
  166. package/dist/esm/json/TimeOnlyJsonConverter.js +17 -0
  167. package/dist/esm/json/TimeOnlyJsonConverter.js.map +1 -0
  168. package/dist/esm/json/index.d.ts +2 -0
  169. package/dist/esm/json/index.d.ts.map +1 -1
  170. package/dist/esm/json/index.js +11 -0
  171. package/dist/esm/json/index.js.map +1 -0
  172. package/dist/esm/typeKey.d.ts +7 -0
  173. package/dist/esm/typeKey.d.ts.map +1 -0
  174. package/dist/esm/typeKey.js +8 -0
  175. package/dist/esm/typeKey.js.map +1 -0
  176. package/dist/geospatial/LineString.d.ts +2 -0
  177. package/dist/geospatial/LineString.d.ts.map +1 -1
  178. package/dist/geospatial/LineString.js +2 -0
  179. package/dist/geospatial/LineString.js.map +1 -1
  180. package/dist/geospatial/Point.d.ts +2 -0
  181. package/dist/geospatial/Point.d.ts.map +1 -1
  182. package/dist/geospatial/Point.js +2 -0
  183. package/dist/geospatial/Point.js.map +1 -1
  184. package/dist/geospatial/Polygon.d.ts +2 -0
  185. package/dist/geospatial/Polygon.d.ts.map +1 -1
  186. package/dist/geospatial/Polygon.js +2 -0
  187. package/dist/geospatial/Polygon.js.map +1 -1
  188. package/dist/index.d.ts +3 -0
  189. package/dist/index.d.ts.map +1 -1
  190. package/dist/index.js +3 -0
  191. package/dist/index.js.map +1 -1
  192. package/dist/json/DateOnlyJsonConverter.d.ts +9 -0
  193. package/dist/json/DateOnlyJsonConverter.d.ts.map +1 -0
  194. package/dist/json/DateOnlyJsonConverter.js +14 -0
  195. package/dist/json/DateOnlyJsonConverter.js.map +1 -0
  196. package/dist/json/JsonConverter.d.ts.map +1 -1
  197. package/dist/json/JsonConverter.js.map +1 -1
  198. package/dist/json/TimeOnlyJsonConverter.d.ts +9 -0
  199. package/dist/json/TimeOnlyJsonConverter.d.ts.map +1 -0
  200. package/dist/json/TimeOnlyJsonConverter.js +14 -0
  201. package/dist/json/TimeOnlyJsonConverter.js.map +1 -0
  202. package/dist/json/index.d.ts +2 -0
  203. package/dist/json/index.d.ts.map +1 -1
  204. package/dist/json/index.js +2 -0
  205. package/dist/json/index.js.map +1 -1
  206. package/dist/tsconfig.tsbuildinfo +1 -1
  207. package/dist/typeKey.d.ts +7 -0
  208. package/dist/typeKey.d.ts.map +1 -0
  209. package/dist/typeKey.js +6 -0
  210. package/dist/typeKey.js.map +1 -0
  211. package/duplicateInstanceGuard.ts +114 -0
  212. package/for_DateOnly/when_parsing_a_calendar_date.ts +13 -0
  213. package/for_DateOnly/when_reading_it_west_of_utc.ts +24 -0
  214. package/for_JsonSerializer/RegisteredConverterTypes.ts +45 -0
  215. package/for_JsonSerializer/when_a_converter_is_registered_for_a_concept.ts +46 -0
  216. package/for_JsonSerializer/when_a_converter_is_registered_for_a_type_that_already_has_one.ts +25 -0
  217. package/for_JsonSerializer/when_a_converter_is_registered_for_a_type_the_package_already_converts.ts +51 -0
  218. package/for_JsonSerializer/when_a_converter_is_registered_for_a_value_map.ts +49 -0
  219. package/for_JsonSerializer/when_a_converter_is_registered_for_an_unknown_type.ts +30 -0
  220. package/for_JsonSerializer/when_a_type_comes_from_another_copy_of_the_package.ts +63 -0
  221. package/for_JsonSerializer/when_deserializing_object_with_absent_collection.ts +36 -0
  222. package/for_JsonSerializer/when_deserializing_temporal_values.ts +31 -0
  223. package/for_JsonSerializer/when_metadata_is_written_by_another_copy_of_the_package.ts +46 -0
  224. package/for_JsonSerializer/when_no_converter_is_registered_for_a_type.ts +32 -0
  225. package/for_JsonSerializer/when_round_tripping_a_value_map_of_primitive_values.ts +35 -0
  226. package/for_JsonSerializer/when_round_tripping_concepts_outside_a_declared_field.ts +39 -0
  227. package/for_TimeOnly/when_parsing_a_time_of_day.ts +17 -0
  228. package/for_TimeOnly/when_parsing_the_shapes_the_server_sends.ts +24 -0
  229. package/for_duplicateInstanceGuard/given/a_realm.ts +62 -0
  230. package/for_duplicateInstanceGuard/when_a_second_copy_is_loaded.ts +28 -0
  231. package/for_duplicateInstanceGuard/when_a_third_copy_is_loaded.ts +20 -0
  232. package/for_duplicateInstanceGuard/when_checking_the_package_manifest.ts +26 -0
  233. package/for_duplicateInstanceGuard/when_one_copy_is_loaded.ts +17 -0
  234. package/for_duplicateInstanceGuard/when_one_copy_registers_more_than_once.ts +20 -0
  235. package/for_duplicateInstanceGuard/when_something_else_holds_the_global_key.ts +25 -0
  236. package/for_duplicateInstanceGuard/when_the_runtime_has_no_console.ts +30 -0
  237. package/geospatial/LineString.ts +3 -0
  238. package/geospatial/Point.ts +3 -0
  239. package/geospatial/Polygon.ts +3 -0
  240. package/index.ts +3 -0
  241. package/json/DateOnlyJsonConverter.ts +28 -0
  242. package/json/JsonConverter.ts +5 -0
  243. package/json/TimeOnlyJsonConverter.ts +28 -0
  244. package/json/index.ts +2 -0
  245. package/package.json +1 -1
  246. package/typeKey.ts +54 -0
@@ -0,0 +1,62 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ const loadedInstancesKey = Symbol.for('@cratis/fundamentals.loadedInstances');
5
+ const duplicateReportedKey = Symbol.for('@cratis/fundamentals.duplicateReported');
6
+
7
+ const globals = globalThis as Record<symbol, unknown>;
8
+
9
+ /** The part of the guard's surface a spec drives. */
10
+ type PackageCopy = {
11
+ registerModuleInstance(): void;
12
+ };
13
+
14
+ let copiesLoaded = 0;
15
+
16
+ /**
17
+ * A realm holding no copies of the package, with `console.warn` captured.
18
+ *
19
+ * The guard keeps its bookkeeping on `globalThis` and its per-copy state in module scope, so a
20
+ * spec has to control both: the global keys are saved and cleared here, and every copy is
21
+ * loaded fresh through {@link loadCopy}.
22
+ */
23
+ export class a_realm {
24
+ /** Every diagnostic written while this realm is in scope. */
25
+ readonly warnings: string[] = [];
26
+
27
+ private readonly originalWarn = console.warn;
28
+ private readonly savedInstances = globals[loadedInstancesKey];
29
+ private readonly savedReported = globals[duplicateReportedKey];
30
+
31
+ constructor() {
32
+ delete globals[loadedInstancesKey];
33
+ delete globals[duplicateReportedKey];
34
+ console.warn = (...values: unknown[]) => { this.warnings.push(values.join(' ')); };
35
+ }
36
+
37
+ /**
38
+ * Loads a copy of the package with its own module scope, the way a package manager
39
+ * produces one by nesting a second physical install.
40
+ *
41
+ * Each call resolves a distinct module identifier, because a module that resolves to an
42
+ * identifier already in the graph is returned from cache rather than evaluated again - and
43
+ * an evaluation is exactly what a second copy is. The counter is module scope shared by
44
+ * every spec file, so identifiers stay distinct across the whole run.
45
+ *
46
+ * `@vite-ignore` keeps the identifier out of Vite's dynamic-import-vars pass, which would
47
+ * otherwise try to resolve the variable part by globbing the file system and fail: the
48
+ * variation here is in the query, not in the path.
49
+ * @returns {Promise<PackageCopy>} A copy that has not registered itself yet.
50
+ */
51
+ async loadCopy(): Promise<PackageCopy> {
52
+ copiesLoaded++;
53
+ return await import(/* @vite-ignore */ `../../duplicateInstanceGuard?copy=${copiesLoaded}`) as PackageCopy;
54
+ }
55
+
56
+ /** Puts back everything the realm took over, so the next spec file starts from the real state. */
57
+ release() {
58
+ console.warn = this.originalWarn;
59
+ globals[loadedInstancesKey] = this.savedInstances;
60
+ globals[duplicateReportedKey] = this.savedReported;
61
+ }
62
+ }
@@ -0,0 +1,28 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { a_realm } from './given/a_realm';
5
+
6
+ describe('when a second copy is loaded', () => {
7
+ let realm: a_realm;
8
+
9
+ beforeEach(async () => {
10
+ realm = new a_realm();
11
+ (await realm.loadCopy()).registerModuleInstance();
12
+ (await realm.loadCopy()).registerModuleInstance();
13
+ });
14
+
15
+ afterEach(() => realm.release());
16
+
17
+ it('should warn exactly once', () => realm.warnings.length.should.equal(1));
18
+ it('should name the package', () => realm.warnings[0].should.contain('@cratis/fundamentals'));
19
+ it('should say how many times it was loaded', () => realm.warnings[0].should.contain('Loaded 2 times'));
20
+ it('should say a registered converter reaches only one copy', () => realm.warnings[0].should.contain('reaches only the copy it was registered on'));
21
+ it('should say instanceof breaks across the copies', () => realm.warnings[0].should.contain('instanceof against the other copy'));
22
+ it('should say a top level pin does not reach the nested copy', () => realm.warnings[0].should.contain('does not reach a copy nested under a dependency'));
23
+ it('should say how to collapse two installs', () => realm.warnings[0].should.contain('yarn dedupe @cratis/fundamentals'));
24
+ it('should say that dedupe is the wrong answer for one install loaded as both formats', () => realm.warnings[0].should.contain('no dedupe will fix'));
25
+ it('should name where the first copy was loaded from', () => realm.warnings[0].should.contain(' 1. '));
26
+ it('should name where the second copy was loaded from', () => realm.warnings[0].should.contain(' 2. '));
27
+ it('should locate the copies rather than give up on the stack', () => realm.warnings[0].should.not.contain('unknown location'));
28
+ });
@@ -0,0 +1,20 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { a_realm } from './given/a_realm';
5
+
6
+ describe('when a third copy is loaded', () => {
7
+ let realm: a_realm;
8
+
9
+ beforeEach(async () => {
10
+ realm = new a_realm();
11
+ (await realm.loadCopy()).registerModuleInstance();
12
+ (await realm.loadCopy()).registerModuleInstance();
13
+ (await realm.loadCopy()).registerModuleInstance();
14
+ });
15
+
16
+ afterEach(() => realm.release());
17
+
18
+ it('should still warn exactly once', () => realm.warnings.length.should.equal(1));
19
+ it('should report the count it knew when it warned', () => realm.warnings[0].should.contain('Loaded 2 times'));
20
+ });
@@ -0,0 +1,26 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { readFileSync } from 'node:fs';
5
+ import { dirname, join } from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ // The guard registers each copy as a load-bearing side effect at module scope, so a bundler must never be told
9
+ // it is free to drop it. Declaring `"sideEffects": false` is the one manifest change that would disable
10
+ // duplicate detection without breaking anything: no build fails, no other spec notices, and a duplicated
11
+ // package quietly goes back to being invisible - which is the failure the guard exists to make visible.
12
+ //
13
+ // duplicateInstanceGuard.ts asks for this in a comment. A comment does not survive a dependency bump or a
14
+ // bundle-size pass, so this is what actually holds the line.
15
+ describe('when checking the package manifest', () => {
16
+ const manifest = JSON.parse(
17
+ readFileSync(join(dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf-8')) as {
18
+ sideEffects?: unknown;
19
+ };
20
+
21
+ // Absent is correct: a bundler then treats the package as side-effectful. An explicit `true` is fine too.
22
+ // Only `false` is the problem, so only `false` is rejected.
23
+ const declaresNoSideEffects = manifest.sideEffects === false;
24
+
25
+ it('should not declare the package free of side effects', () => declaresNoSideEffects.should.be.false);
26
+ });
@@ -0,0 +1,17 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { a_realm } from './given/a_realm';
5
+
6
+ describe('when one copy is loaded', () => {
7
+ let realm: a_realm;
8
+
9
+ beforeEach(async () => {
10
+ realm = new a_realm();
11
+ (await realm.loadCopy()).registerModuleInstance();
12
+ });
13
+
14
+ afterEach(() => realm.release());
15
+
16
+ it('should not warn', () => realm.warnings.should.be.empty);
17
+ });
@@ -0,0 +1,20 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { a_realm } from './given/a_realm';
5
+
6
+ describe('when one copy registers more than once', () => {
7
+ let realm: a_realm;
8
+
9
+ beforeEach(async () => {
10
+ realm = new a_realm();
11
+ const copy = await realm.loadCopy();
12
+ copy.registerModuleInstance();
13
+ copy.registerModuleInstance();
14
+ copy.registerModuleInstance();
15
+ });
16
+
17
+ afterEach(() => realm.release());
18
+
19
+ it('should not warn', () => realm.warnings.should.be.empty);
20
+ });
@@ -0,0 +1,25 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { a_realm } from './given/a_realm';
5
+
6
+ describe('when something else holds the global key', () => {
7
+ let realm: a_realm;
8
+
9
+ beforeEach(async () => {
10
+ realm = new a_realm();
11
+
12
+ // The bookkeeping is a cross-version contract, so a copy of a different version of this package
13
+ // may have written the key first - in a shape this one does not recognize. Detection has to
14
+ // survive that rather than take the process down over its own diagnostic.
15
+ (globalThis as Record<symbol, unknown>)[Symbol.for('@cratis/fundamentals.loadedInstances')] = 'written by something else';
16
+
17
+ (await realm.loadCopy()).registerModuleInstance();
18
+ (await realm.loadCopy()).registerModuleInstance();
19
+ });
20
+
21
+ afterEach(() => realm.release());
22
+
23
+ it('should still report the duplicate', () => realm.warnings.length.should.equal(1));
24
+ it('should count only the copies it registered itself', () => realm.warnings[0].should.contain('Loaded 2 times'));
25
+ });
@@ -0,0 +1,30 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { a_realm } from './given/a_realm';
5
+
6
+ /** A console that is missing the method the report writes to. */
7
+ type ConsoleWithoutWarn = { warn?: (...values: unknown[]) => void };
8
+
9
+ describe('when the runtime has no console', () => {
10
+ let realm: a_realm;
11
+ let error: unknown;
12
+
13
+ beforeEach(async () => {
14
+ realm = new a_realm();
15
+ (console as unknown as ConsoleWithoutWarn).warn = undefined;
16
+
17
+ try {
18
+ (await realm.loadCopy()).registerModuleInstance();
19
+ (await realm.loadCopy()).registerModuleInstance();
20
+ } catch (thrown) {
21
+ error = thrown;
22
+ }
23
+ });
24
+
25
+ afterEach(() => realm.release());
26
+
27
+ // A diagnostic that takes the process down when it has nowhere to write is worse than the silence
28
+ // it was added to break.
29
+ it('should not throw', () => (error === undefined).should.be.true);
30
+ });
@@ -2,11 +2,14 @@
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
4
  import { Point } from './Point';
5
+ import { typeKey } from '../typeKey';
5
6
 
6
7
  /**
7
8
  * Represents a line string composed of two or more points.
8
9
  */
9
10
  export class LineString {
11
+ static readonly [typeKey] = 'LineString';
12
+
10
13
  coordinates!: Point[];
11
14
 
12
15
  /**
@@ -2,11 +2,14 @@
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
4
  import { field } from '../fieldDecorator';
5
+ import { typeKey } from '../typeKey';
5
6
 
6
7
  /**
7
8
  * Represents a geographic point with longitude and latitude.
8
9
  */
9
10
  export class Point {
11
+ static readonly [typeKey] = 'Point';
12
+
10
13
  @field(Number)
11
14
  longitude!: number;
12
15
 
@@ -2,11 +2,14 @@
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
4
  import { LinearRing } from './LinearRing';
5
+ import { typeKey } from '../typeKey';
5
6
 
6
7
  /**
7
8
  * Represents a polygon with an outer shell and optional holes.
8
9
  */
9
10
  export class Polygon {
11
+ static readonly [typeKey] = 'Polygon';
12
+
10
13
  shell!: LinearRing;
11
14
  holes!: LinearRing[];
12
15
 
package/index.ts CHANGED
@@ -7,13 +7,16 @@ export * from './geospatial';
7
7
  export * from './json';
8
8
  export * from './Field';
9
9
  export * from './Fields';
10
+ export * from './DateOnly';
10
11
  export * from './Guid';
11
12
  export * from './IEquatable';
12
13
  export * from './PropertyAccessor';
13
14
  export * from './PropertyAccessorDescriptor';
14
15
  export * from './PropertyPathResolverProxyHandler';
16
+ export * from './TimeOnly';
15
17
  export * from './TimeSpan';
16
18
  export * from './ValueMap';
19
+ export * from './typeKey';
17
20
  export * from './fieldDecorator';
18
21
  export * from './derivedTypeDecorator';
19
22
  export * from './JsonSerializer';
@@ -0,0 +1,28 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { Constructor } from '../Constructor';
5
+ import { DateOnly } from '../DateOnly';
6
+ import { JsonConverter } from './JsonConverter';
7
+
8
+ /* eslint-disable @typescript-eslint/no-explicit-any */
9
+
10
+ /**
11
+ * JSON converter for DateOnly type.
12
+ */
13
+ export class DateOnlyJsonConverter extends JsonConverter<DateOnly> {
14
+ /** @inheritdoc */
15
+ get type(): Constructor<DateOnly> {
16
+ return DateOnly;
17
+ }
18
+
19
+ /** @inheritdoc */
20
+ read(value: any): DateOnly {
21
+ return DateOnly.parse(value.toString());
22
+ }
23
+
24
+ /** @inheritdoc */
25
+ write(value: any): any {
26
+ return value?.toString() ?? '';
27
+ }
28
+ }
@@ -19,6 +19,11 @@ export abstract class JsonConverter<T = any> {
19
19
  * Determines whether the converter can convert the specified type.
20
20
  * @param {Constructor} typeToConvert - The type to check.
21
21
  * @returns {boolean} True if the converter can convert the type; otherwise, false.
22
+ * @deprecated Not called by `JsonSerializer` and scheduled for removal in the next major version.
23
+ * A converter is resolved by the type it declares through {@link type}, which is what this would
24
+ * answer anyway, so overriding it has no effect - register a converter per type instead. Wiring it
25
+ * into the lookup is not the plan either: it would force a scan of every converter on the path
26
+ * taken by every plain object, to serve a case a second registration already covers.
22
27
  */
23
28
  canConvert(typeToConvert: Constructor): boolean {
24
29
  return typeToConvert === this.type;
@@ -0,0 +1,28 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { Constructor } from '../Constructor';
5
+ import { TimeOnly } from '../TimeOnly';
6
+ import { JsonConverter } from './JsonConverter';
7
+
8
+ /* eslint-disable @typescript-eslint/no-explicit-any */
9
+
10
+ /**
11
+ * JSON converter for TimeOnly type.
12
+ */
13
+ export class TimeOnlyJsonConverter extends JsonConverter<TimeOnly> {
14
+ /** @inheritdoc */
15
+ get type(): Constructor<TimeOnly> {
16
+ return TimeOnly;
17
+ }
18
+
19
+ /** @inheritdoc */
20
+ read(value: any): TimeOnly {
21
+ return TimeOnly.parse(value.toString());
22
+ }
23
+
24
+ /** @inheritdoc */
25
+ write(value: any): any {
26
+ return value?.toString() ?? '';
27
+ }
28
+ }
package/json/index.ts CHANGED
@@ -3,7 +3,9 @@
3
3
 
4
4
  export * from './JsonConverter';
5
5
  export * from './DateJsonConverter';
6
+ export * from './DateOnlyJsonConverter';
6
7
  export * from './GuidJsonConverter';
8
+ export * from './TimeOnlyJsonConverter';
7
9
  export * from './TimeSpanJsonConverter';
8
10
  export * from './PointJsonConverter';
9
11
  export * from './LineStringJsonConverter';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/fundamentals",
3
- "version": "7.16.7",
3
+ "version": "7.17.0",
4
4
  "description": "",
5
5
  "author": "Cratis",
6
6
  "license": "MIT",
package/typeKey.ts ADDED
@@ -0,0 +1,54 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import { Constructor } from './Constructor';
5
+
6
+ /**
7
+ * Identifies a type this package converts, by name rather than by the class object itself.
8
+ *
9
+ * `JsonSerializer` recognized a type by comparing constructors, which holds until a second copy of this
10
+ * package is loaded into the same realm - a nested install, or one install reached as both ESM and
11
+ * CommonJS. Each copy then had its own `Guid`, `ConceptAs` and `ValueMap` class objects and neither
12
+ * recognized the other's, so a `Guid` crossing that boundary was written as an object rather than a
13
+ * string, and a concept as `{"value": ...}` rather than its underlying value. Both silently.
14
+ *
15
+ * A name survives the boundary where a class object does not: the key is a registered symbol, so every
16
+ * copy resolves the same one, and the values are plain strings.
17
+ *
18
+ * The key is deliberately a symbol rather than a plain static: a string static would appear in
19
+ * `Object.keys(Guid)` and could be shadowed by an unrelated member of the same name.
20
+ */
21
+ export const typeKey: unique symbol = Symbol.for('@cratis/fundamentals.typeKey');
22
+
23
+ /** A type carrying the key. Only this package's own convertible types declare one. */
24
+ type KeyedType = Constructor & { readonly [typeKey]?: string };
25
+
26
+ /**
27
+ * Gets the key a type declares itself, ignoring any inherited from a base type.
28
+ *
29
+ * Converter lookup uses this, because a key is inherited: a consumer's `class CustomerId extends
30
+ * ConceptAs<string>` reports `ConceptAs`, and registering a converter under that would take the place
31
+ * of concept handling for every concept in the application.
32
+ * @param {Constructor} type The type to read the key from.
33
+ * @returns {string | undefined} The declared key, or undefined when the type declares none.
34
+ */
35
+ export const declaredTypeKey = (type: Constructor | undefined): string | undefined =>
36
+ type && Object.prototype.hasOwnProperty.call(type, typeKey) ? (type as KeyedType)[typeKey] : undefined;
37
+
38
+ /**
39
+ * Gets the key a type carries, whether it declares the key or inherits it from a base type.
40
+ *
41
+ * This is what makes a concept recognizable across copies: the key is a static, and a static is
42
+ * inherited through the constructor's prototype chain, so every type deriving from `ConceptAs` reports
43
+ * `ConceptAs` without anything having to compare class objects.
44
+ * @param {Constructor} type The type to read the key from.
45
+ * @returns {string | undefined} The key carried, or undefined when the type carries none.
46
+ */
47
+ export const typeKeyOf = (type: Constructor | undefined): string | undefined =>
48
+ type ? (type as KeyedType)[typeKey] : undefined;
49
+
50
+ /** The key every type deriving from `ConceptAs` carries. */
51
+ export const conceptAsTypeKey = 'ConceptAs';
52
+
53
+ /** The key `ValueMap` declares. */
54
+ export const valueMapTypeKey = 'ValueMap';