@comms-id/asx 0.4.1 → 0.4.2
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/dist/browser-profile.d.ts +21 -0
- package/dist/browser-profile.js +115 -0
- package/dist/client-replies.d.ts +178 -0
- package/dist/client-replies.js +57 -0
- package/dist/client.d.ts +25 -0
- package/dist/client.js +179 -0
- package/dist/collection.d.ts +555 -0
- package/dist/collection.js +194 -0
- package/dist/company-events.d.ts +2 -0
- package/dist/company-events.js +48 -0
- package/dist/company-record.d.ts +4 -0
- package/dist/company-record.js +60 -0
- package/dist/company-stream-client.d.ts +4 -0
- package/dist/company-stream-client.js +139 -0
- package/dist/company-stream.d.ts +46 -0
- package/dist/company-stream.js +117 -0
- package/dist/contract/http-api.d.ts +139 -0
- package/dist/contract/http-api.js +100 -0
- package/dist/directory-search.d.ts +2 -0
- package/dist/directory-search.js +33 -0
- package/dist/enrichment.d.ts +75 -0
- package/dist/enrichment.js +114 -0
- package/dist/http.d.ts +1 -0
- package/dist/http.js +49 -0
- package/dist/live.d.ts +56 -0
- package/dist/live.js +129 -0
- package/dist/model.d.ts +112 -0
- package/dist/model.js +86 -0
- package/dist/parse.d.ts +4 -0
- package/dist/parse.js +130 -0
- package/dist/profile-contact.d.ts +13 -0
- package/dist/profile-contact.js +66 -0
- package/dist/publication.d.ts +25 -0
- package/dist/publication.js +89 -0
- package/dist/refresh-coordinator.d.ts +96 -0
- package/dist/refresh-coordinator.js +194 -0
- package/dist/refresh.d.ts +126 -0
- package/dist/refresh.js +157 -0
- package/dist/requests.d.ts +10 -0
- package/dist/requests.js +21 -0
- package/dist/runtime/http.js +8 -1
- package/dist/search-service.d.ts +16 -0
- package/dist/search-service.js +64 -0
- package/dist/service.d.ts +35 -0
- package/dist/service.js +99 -0
- package/dist/snapshot.d.ts +328 -0
- package/dist/snapshot.js +151 -0
- package/dist/source-record.d.ts +63 -0
- package/dist/source-record.js +91 -0
- package/package.json +1 -1
package/dist/snapshot.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { suggestDirectory } from "./directory-search.js";
|
|
2
|
+
import { Result, Schema } from "effect";
|
|
3
|
+
import { DirectoryEntry, failure, isTimestamp, Observation, validObservation } from "./model.js";
|
|
4
|
+
import { normalizeTicker } from "./parse.js";
|
|
5
|
+
import { ConfirmedEnrichment, validAbn, validEnrichment } from "./enrichment.js";
|
|
6
|
+
import { CompanySources, decodeCompanySources } from "./source-record.js";
|
|
7
|
+
/**
|
|
8
|
+
* An ABN matched to a listed company by the owner-accepted name rules (#385): the
|
|
9
|
+
* full legal name, one trailing company suffix removed, a trailing "(THE)" moved to
|
|
10
|
+
* the front, or a comma ignored. A name match, never proof of issuer identity.
|
|
11
|
+
*/
|
|
12
|
+
export const AbnNameMatch = Schema.Struct({
|
|
13
|
+
abn: Schema.String.check(Schema.makeFilter(validAbn)),
|
|
14
|
+
acn: Schema.NullOr(Schema.String.check(Schema.isPattern(/^\d{9}$/))),
|
|
15
|
+
abnEntityName: Schema.NonEmptyString,
|
|
16
|
+
rule: Schema.Literals(["full-name", "suffix-removed", "the-moved", "comma-ignored"]),
|
|
17
|
+
abnSnapshotDate: Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}$/)),
|
|
18
|
+
});
|
|
19
|
+
export const CompanyRecord = Schema.Struct({
|
|
20
|
+
directory: DirectoryEntry,
|
|
21
|
+
observation: Schema.NullOr(Observation),
|
|
22
|
+
sources: Schema.optionalKey(CompanySources),
|
|
23
|
+
enrichment: Schema.optionalKey(ConfirmedEnrichment),
|
|
24
|
+
abnNameMatch: Schema.optionalKey(AbnNameMatch),
|
|
25
|
+
missingAt: Schema.optionalKey(Schema.Struct({ startedAt: Schema.String, observedAt: Schema.String })),
|
|
26
|
+
});
|
|
27
|
+
const Dataset = Schema.Struct({
|
|
28
|
+
formatVersion: Schema.Literal(1),
|
|
29
|
+
directoryObservedAt: Schema.String,
|
|
30
|
+
records: Schema.Array(CompanyRecord),
|
|
31
|
+
});
|
|
32
|
+
const Generation = Schema.String.check(Schema.isPattern(/^[a-f0-9]{64}$/));
|
|
33
|
+
export const SnapshotOutcome = Schema.Union([
|
|
34
|
+
Schema.Struct({
|
|
35
|
+
status: Schema.Literal("found"),
|
|
36
|
+
observation: Observation,
|
|
37
|
+
enrichment: Schema.optionalKey(ConfirmedEnrichment),
|
|
38
|
+
generation: Generation,
|
|
39
|
+
}),
|
|
40
|
+
Schema.Struct({ status: Schema.Literal("missing"), generation: Generation }),
|
|
41
|
+
Schema.Struct({ status: Schema.Literal("unavailable"), generation: Generation }),
|
|
42
|
+
]);
|
|
43
|
+
const MAX_BYTES = 32 * 1024 * 1024;
|
|
44
|
+
export function decodeDataset(input) {
|
|
45
|
+
const decoded = Schema.decodeUnknownResult(Dataset)(input);
|
|
46
|
+
if (Result.isFailure(decoded))
|
|
47
|
+
throw failure("SNAPSHOT_UNAVAILABLE");
|
|
48
|
+
const dataset = decoded.success;
|
|
49
|
+
if (!isTimestamp(dataset.directoryObservedAt) ||
|
|
50
|
+
dataset.records.length === 0 ||
|
|
51
|
+
dataset.records.length > 20_000)
|
|
52
|
+
throw failure("SNAPSHOT_UNAVAILABLE");
|
|
53
|
+
const tickers = new Set();
|
|
54
|
+
for (const record of dataset.records) {
|
|
55
|
+
if (tickers.has(record.directory.ticker) || !record.directory.companyName.trim())
|
|
56
|
+
throw failure("SNAPSHOT_UNAVAILABLE");
|
|
57
|
+
tickers.add(record.directory.ticker);
|
|
58
|
+
if (record.sources)
|
|
59
|
+
decodeCompanySources(record.sources, record.directory.ticker);
|
|
60
|
+
if (record.enrichment &&
|
|
61
|
+
(!record.observation || !validEnrichment(record.enrichment, record.observation.company)))
|
|
62
|
+
throw failure("SNAPSHOT_UNAVAILABLE");
|
|
63
|
+
if (record.missingAt &&
|
|
64
|
+
(record.observation !== null ||
|
|
65
|
+
!isTimestamp(record.missingAt.startedAt) ||
|
|
66
|
+
!isTimestamp(record.missingAt.observedAt) ||
|
|
67
|
+
record.missingAt.startedAt > record.missingAt.observedAt))
|
|
68
|
+
throw failure("SNAPSHOT_UNAVAILABLE");
|
|
69
|
+
if (record.observation &&
|
|
70
|
+
(!validObservation(record.observation) ||
|
|
71
|
+
record.observation.company.ticker !== record.directory.ticker))
|
|
72
|
+
throw failure("SNAPSHOT_UNAVAILABLE");
|
|
73
|
+
}
|
|
74
|
+
return dataset;
|
|
75
|
+
}
|
|
76
|
+
async function digest(text) {
|
|
77
|
+
const bytes = new TextEncoder().encode(text);
|
|
78
|
+
if (bytes.byteLength > MAX_BYTES)
|
|
79
|
+
throw failure("SNAPSHOT_UNAVAILABLE");
|
|
80
|
+
const hash = await crypto.subtle.digest("SHA-256", bytes);
|
|
81
|
+
return Array.from(new Uint8Array(hash), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
82
|
+
}
|
|
83
|
+
/** Build content-addressed bytes; the host must verify its write before selection. */
|
|
84
|
+
export async function encodeSnapshot(input) {
|
|
85
|
+
const dataset = decodeDataset(input);
|
|
86
|
+
const text = JSON.stringify(dataset);
|
|
87
|
+
return { generation: await digest(text), text };
|
|
88
|
+
}
|
|
89
|
+
/** The trusted host pins the expected generation; body content cannot choose it. */
|
|
90
|
+
export async function openSnapshot(text, expectedGeneration) {
|
|
91
|
+
if (!/^[a-f0-9]{64}$/.test(expectedGeneration) ||
|
|
92
|
+
typeof text !== "string" ||
|
|
93
|
+
text.length > MAX_BYTES)
|
|
94
|
+
throw failure("SNAPSHOT_UNAVAILABLE");
|
|
95
|
+
if ((await digest(text)) !== expectedGeneration)
|
|
96
|
+
throw failure("SNAPSHOT_UNAVAILABLE");
|
|
97
|
+
let input;
|
|
98
|
+
try {
|
|
99
|
+
input = JSON.parse(text);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
throw failure("SNAPSHOT_UNAVAILABLE");
|
|
103
|
+
}
|
|
104
|
+
const dataset = decodeDataset(input);
|
|
105
|
+
const records = new Map(dataset.records.map((record) => [record.directory.ticker, record]));
|
|
106
|
+
return {
|
|
107
|
+
generation: expectedGeneration,
|
|
108
|
+
record: (ticker) => structuredClone(records.get(normalizeTicker(ticker)) ?? null),
|
|
109
|
+
lookup: async (inputTicker, signal) => {
|
|
110
|
+
if (signal?.aborted)
|
|
111
|
+
throw failure("CANCELLED");
|
|
112
|
+
const record = records.get(normalizeTicker(inputTicker));
|
|
113
|
+
if (!record)
|
|
114
|
+
return { status: "missing", generation: expectedGeneration };
|
|
115
|
+
if (record.missingAt)
|
|
116
|
+
return { status: "missing", generation: expectedGeneration };
|
|
117
|
+
if (!record.observation)
|
|
118
|
+
return { status: "unavailable", generation: expectedGeneration };
|
|
119
|
+
return {
|
|
120
|
+
status: "found",
|
|
121
|
+
observation: structuredClone(record.observation),
|
|
122
|
+
...(record.enrichment ? { enrichment: structuredClone(record.enrichment) } : {}),
|
|
123
|
+
generation: expectedGeneration,
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
suggest: (query, limit = 8) => suggestDirectory(dataset.records.map((record) => record.directory), query, limit),
|
|
127
|
+
findByAbn: (abn) => {
|
|
128
|
+
if (typeof abn !== "string" || !validAbn(abn))
|
|
129
|
+
throw failure("INVALID_INPUT");
|
|
130
|
+
return dataset.records.flatMap((record) => record.enrichment?.abn === abn
|
|
131
|
+
? [{ directory: { ...record.directory }, enrichment: structuredClone(record.enrichment) }]
|
|
132
|
+
: []);
|
|
133
|
+
},
|
|
134
|
+
findNameMatchesByAbn: (abn) => {
|
|
135
|
+
if (typeof abn !== "string" || !validAbn(abn))
|
|
136
|
+
throw failure("INVALID_INPUT");
|
|
137
|
+
return dataset.records.flatMap((record) => record.abnNameMatch?.abn === abn
|
|
138
|
+
? [{ directory: { ...record.directory }, nameMatch: { ...record.abnNameMatch } }]
|
|
139
|
+
: []);
|
|
140
|
+
},
|
|
141
|
+
metadata: () => ({
|
|
142
|
+
directoryObservedAt: dataset.directoryObservedAt,
|
|
143
|
+
records: records.size,
|
|
144
|
+
availableProfiles: dataset.records.filter((record) => record.observation !== null).length,
|
|
145
|
+
confirmedAbns: dataset.records.filter((record) => record.enrichment !== undefined).length,
|
|
146
|
+
unresolvedAbns: dataset.records.filter((record) => record.enrichment === undefined).length,
|
|
147
|
+
confirmedWebsites: dataset.records.filter((record) => record.enrichment?.website != null)
|
|
148
|
+
.length,
|
|
149
|
+
}),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import { type LiveOutcome } from "./model.js";
|
|
3
|
+
export declare const ASX_COMPANY_BASE = "https://asx.api.markitdigital.com/asx-research/1.0/companies/";
|
|
4
|
+
export declare function asxSourceObservation(ticker: string, result: LiveOutcome): SourceObservation;
|
|
5
|
+
export declare const SourceName: Schema.Literals<readonly ["asx", "morningstar", "google-finance"]>;
|
|
6
|
+
export type SourceName = typeof SourceName.Type;
|
|
7
|
+
export declare const SOURCE_NAMES: readonly SourceName[];
|
|
8
|
+
/** Provider data remains separate. These fields do not select a preferred website. */
|
|
9
|
+
export declare const SourceObservation: Schema.Struct<{
|
|
10
|
+
readonly source: Schema.Literals<readonly ["asx", "morningstar", "google-finance"]>;
|
|
11
|
+
readonly ticker: Schema.String;
|
|
12
|
+
readonly sourceUrl: Schema.String;
|
|
13
|
+
readonly sourceId: Schema.NonEmptyString;
|
|
14
|
+
readonly status: Schema.Literals<readonly ["found", "missing"]>;
|
|
15
|
+
readonly startedAt: Schema.String;
|
|
16
|
+
readonly observedAt: Schema.String;
|
|
17
|
+
readonly payload: Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>;
|
|
18
|
+
readonly websites: Schema.$Array<Schema.String>;
|
|
19
|
+
}>;
|
|
20
|
+
export interface SourceObservation extends Schema.Schema.Type<typeof SourceObservation> {
|
|
21
|
+
}
|
|
22
|
+
export declare const CompanySources: Schema.Struct<{
|
|
23
|
+
readonly asx: Schema.optionalKey<Schema.Struct<{
|
|
24
|
+
readonly source: Schema.Literals<readonly ["asx", "morningstar", "google-finance"]>;
|
|
25
|
+
readonly ticker: Schema.String;
|
|
26
|
+
readonly sourceUrl: Schema.String;
|
|
27
|
+
readonly sourceId: Schema.NonEmptyString;
|
|
28
|
+
readonly status: Schema.Literals<readonly ["found", "missing"]>;
|
|
29
|
+
readonly startedAt: Schema.String;
|
|
30
|
+
readonly observedAt: Schema.String;
|
|
31
|
+
readonly payload: Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>;
|
|
32
|
+
readonly websites: Schema.$Array<Schema.String>;
|
|
33
|
+
}>>;
|
|
34
|
+
readonly morningstar: Schema.optionalKey<Schema.Struct<{
|
|
35
|
+
readonly source: Schema.Literals<readonly ["asx", "morningstar", "google-finance"]>;
|
|
36
|
+
readonly ticker: Schema.String;
|
|
37
|
+
readonly sourceUrl: Schema.String;
|
|
38
|
+
readonly sourceId: Schema.NonEmptyString;
|
|
39
|
+
readonly status: Schema.Literals<readonly ["found", "missing"]>;
|
|
40
|
+
readonly startedAt: Schema.String;
|
|
41
|
+
readonly observedAt: Schema.String;
|
|
42
|
+
readonly payload: Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>;
|
|
43
|
+
readonly websites: Schema.$Array<Schema.String>;
|
|
44
|
+
}>>;
|
|
45
|
+
readonly "google-finance": Schema.optionalKey<Schema.Struct<{
|
|
46
|
+
readonly source: Schema.Literals<readonly ["asx", "morningstar", "google-finance"]>;
|
|
47
|
+
readonly ticker: Schema.String;
|
|
48
|
+
readonly sourceUrl: Schema.String;
|
|
49
|
+
readonly sourceId: Schema.NonEmptyString;
|
|
50
|
+
readonly status: Schema.Literals<readonly ["found", "missing"]>;
|
|
51
|
+
readonly startedAt: Schema.String;
|
|
52
|
+
readonly observedAt: Schema.String;
|
|
53
|
+
readonly payload: Schema.$Record<Schema.String, Schema.Codec<Schema.Json, Schema.Json, never, never>>;
|
|
54
|
+
readonly websites: Schema.$Array<Schema.String>;
|
|
55
|
+
}>>;
|
|
56
|
+
}>;
|
|
57
|
+
export interface CompanySources extends Schema.Schema.Type<typeof CompanySources> {
|
|
58
|
+
}
|
|
59
|
+
/** Decode at every collector, storage and consumer boundary; never discard unknown payload fields. */
|
|
60
|
+
export declare function decodeSourceObservation(input: unknown, ticker: string, source: SourceName): SourceObservation;
|
|
61
|
+
export declare function decodeCompanySources(input: unknown, ticker: string): CompanySources;
|
|
62
|
+
/** Only a newer observation of this source can replace it. Failure is not an observation. */
|
|
63
|
+
export declare function mergeCompanySources(ticker: string, previous: CompanySources, incoming: CompanySources): CompanySources;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Result, Schema } from "effect";
|
|
2
|
+
import { failure, isTimestamp, Ticker, Timestamp } from "./model.js";
|
|
3
|
+
export const ASX_COMPANY_BASE = "https://asx.api.markitdigital.com/asx-research/1.0/companies/";
|
|
4
|
+
export function asxSourceObservation(ticker, result) {
|
|
5
|
+
const found = result.status === "found" ? result.observation : null;
|
|
6
|
+
return decodeSourceObservation({
|
|
7
|
+
ticker,
|
|
8
|
+
source: "asx",
|
|
9
|
+
sourceId: found?.company.sourceId ?? ticker,
|
|
10
|
+
sourceUrl: `${ASX_COMPANY_BASE}${ticker}/about`,
|
|
11
|
+
status: result.status,
|
|
12
|
+
startedAt: found?.startedAt ?? (result.status === "missing" ? result.startedAt : ""),
|
|
13
|
+
observedAt: found?.observedAt ?? (result.status === "missing" ? result.observedAt : ""),
|
|
14
|
+
payload: found?.company.raw ?? (result.status === "missing" ? result.raw : undefined),
|
|
15
|
+
websites: found?.company.website ? [found.company.website] : [],
|
|
16
|
+
}, ticker, "asx");
|
|
17
|
+
}
|
|
18
|
+
export const SourceName = Schema.Literals(["asx", "morningstar", "google-finance"]);
|
|
19
|
+
export const SOURCE_NAMES = ["asx", "morningstar", "google-finance"];
|
|
20
|
+
/** Provider data remains separate. These fields do not select a preferred website. */
|
|
21
|
+
export const SourceObservation = Schema.Struct({
|
|
22
|
+
source: SourceName,
|
|
23
|
+
ticker: Ticker,
|
|
24
|
+
sourceUrl: Schema.String,
|
|
25
|
+
sourceId: Schema.NonEmptyString,
|
|
26
|
+
status: Schema.Literals(["found", "missing"]),
|
|
27
|
+
startedAt: Timestamp,
|
|
28
|
+
observedAt: Timestamp,
|
|
29
|
+
payload: Schema.JsonObject,
|
|
30
|
+
websites: Schema.Array(Schema.String).check(Schema.isMaxLength(100)),
|
|
31
|
+
});
|
|
32
|
+
export const CompanySources = Schema.Struct({
|
|
33
|
+
asx: Schema.optionalKey(SourceObservation),
|
|
34
|
+
morningstar: Schema.optionalKey(SourceObservation),
|
|
35
|
+
"google-finance": Schema.optionalKey(SourceObservation),
|
|
36
|
+
});
|
|
37
|
+
function publicUrl(value) {
|
|
38
|
+
try {
|
|
39
|
+
const url = new URL(value);
|
|
40
|
+
return ["http:", "https:"].includes(url.protocol) && !url.username && !url.password;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Decode at every collector, storage and consumer boundary; never discard unknown payload fields. */
|
|
47
|
+
export function decodeSourceObservation(input, ticker, source) {
|
|
48
|
+
const result = Schema.decodeUnknownResult(SourceObservation)(input);
|
|
49
|
+
if (Result.isFailure(result))
|
|
50
|
+
throw failure("PARSE_FAILED");
|
|
51
|
+
const value = result.success;
|
|
52
|
+
if (value.ticker !== ticker || value.source !== source)
|
|
53
|
+
throw failure("SOURCE_MISMATCH");
|
|
54
|
+
if (!isTimestamp(value.startedAt) ||
|
|
55
|
+
!isTimestamp(value.observedAt) ||
|
|
56
|
+
value.startedAt > value.observedAt ||
|
|
57
|
+
!publicUrl(value.sourceUrl) ||
|
|
58
|
+
value.websites.some((url) => !publicUrl(url)) ||
|
|
59
|
+
(value.status === "missing" && value.websites.length > 0) ||
|
|
60
|
+
new TextEncoder().encode(JSON.stringify(value)).byteLength > 1024 * 1024)
|
|
61
|
+
throw failure("PARSE_FAILED");
|
|
62
|
+
return structuredClone(value);
|
|
63
|
+
}
|
|
64
|
+
export function decodeCompanySources(input, ticker) {
|
|
65
|
+
const decoded = Schema.decodeUnknownResult(CompanySources)(input);
|
|
66
|
+
if (Result.isFailure(decoded))
|
|
67
|
+
throw failure("PARSE_FAILED");
|
|
68
|
+
const sources = {};
|
|
69
|
+
for (const source of SOURCE_NAMES) {
|
|
70
|
+
const value = decoded.success[source];
|
|
71
|
+
if (value)
|
|
72
|
+
sources[source] = decodeSourceObservation(value, ticker, source);
|
|
73
|
+
}
|
|
74
|
+
return sources;
|
|
75
|
+
}
|
|
76
|
+
/** Only a newer observation of this source can replace it. Failure is not an observation. */
|
|
77
|
+
export function mergeCompanySources(ticker, previous, incoming) {
|
|
78
|
+
const merged = Object.assign({}, decodeCompanySources(previous, ticker));
|
|
79
|
+
const next = decodeCompanySources(incoming, ticker);
|
|
80
|
+
for (const source of SOURCE_NAMES) {
|
|
81
|
+
const value = next[source];
|
|
82
|
+
const prior = merged[source];
|
|
83
|
+
if (value &&
|
|
84
|
+
(!prior ||
|
|
85
|
+
value.startedAt > prior.startedAt ||
|
|
86
|
+
(value.startedAt === prior.startedAt && value.observedAt > prior.observedAt))) {
|
|
87
|
+
merged[source] = value;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return merged;
|
|
91
|
+
}
|