creek 0.2.2 → 0.3.0-alpha.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/commands/deploy.d.ts +8 -0
- package/dist/commands/deploy.js +146 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.js +60 -0
- package/dist/commands/login.d.ts +8 -0
- package/dist/commands/login.js +32 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +19 -0
- package/dist/utils/bundle.d.ts +6 -0
- package/dist/utils/bundle.js +24 -0
- package/dist/utils/config.d.ts +11 -0
- package/dist/utils/config.js +33 -0
- package/dist/utils/ssr-bundle.d.ts +6 -0
- package/dist/utils/ssr-bundle.js +48 -0
- package/package.json +38 -16
- package/README.md +0 -105
- package/bin/creek +0 -10
- package/examples/hello.creek +0 -6
- package/examples/twitter.creek +0 -23
- package/examples/zeromq.creek +0 -16
- package/lib/aggregator.coffee +0 -27
- package/lib/aggregators/count.coffee +0 -15
- package/lib/aggregators/distinct.coffee +0 -20
- package/lib/aggregators/max.coffee +0 -16
- package/lib/aggregators/mean.coffee +0 -27
- package/lib/aggregators/min.coffee +0 -16
- package/lib/aggregators/popular.coffee +0 -27
- package/lib/aggregators/recent.coffee +0 -10
- package/lib/aggregators/sum.coffee +0 -17
- package/lib/bootstrap.js +0 -5
- package/lib/compound-aggregator.coffee +0 -33
- package/lib/creek.coffee +0 -13
- package/lib/interfaces/rest.coffee +0 -30
- package/lib/interfaces/websocket.coffee +0 -15
- package/lib/lazy-bucketed-aggregator.coffee +0 -35
- package/lib/parsers/chunked.coffee +0 -17
- package/lib/parsers/json.coffee +0 -9
- package/lib/parsers/words.coffee +0 -6
- package/lib/parsers/zeromq.coffee +0 -16
- package/lib/runner.coffee +0 -39
- package/lib/timeboxed-aggregator.coffee +0 -64
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import consola from "consola";
|
|
3
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { execSync } from "node:child_process";
|
|
6
|
+
import { parseConfig, CreekClient, isSSRFramework, getSSRServerEntry, getClientAssetsDir, } from "@solcreek/sdk";
|
|
7
|
+
import { getToken, getApiUrl } from "../utils/config.js";
|
|
8
|
+
import { collectAssets } from "../utils/bundle.js";
|
|
9
|
+
import { bundleSSRServer } from "../utils/ssr-bundle.js";
|
|
10
|
+
export const deployCommand = defineCommand({
|
|
11
|
+
meta: {
|
|
12
|
+
name: "deploy",
|
|
13
|
+
description: "Deploy the current project to Creek",
|
|
14
|
+
},
|
|
15
|
+
args: {
|
|
16
|
+
"skip-build": {
|
|
17
|
+
type: "boolean",
|
|
18
|
+
description: "Skip the build step",
|
|
19
|
+
default: false,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
async run({ args }) {
|
|
23
|
+
const cwd = process.cwd();
|
|
24
|
+
const configPath = join(cwd, "creek.toml");
|
|
25
|
+
if (!existsSync(configPath)) {
|
|
26
|
+
consola.error("No creek.toml found. Run `creek init` first.");
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const config = parseConfig(readFileSync(configPath, "utf-8"));
|
|
30
|
+
consola.info(`Project: ${config.project.name}`);
|
|
31
|
+
const token = getToken();
|
|
32
|
+
if (!token) {
|
|
33
|
+
consola.error("Not authenticated. Run `creek login` first.");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
const client = new CreekClient(getApiUrl(), token);
|
|
37
|
+
// Ensure project exists
|
|
38
|
+
let project;
|
|
39
|
+
try {
|
|
40
|
+
project = await client.getProject(config.project.name);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
consola.info("Project not found, creating...");
|
|
44
|
+
const res = await client.createProject({
|
|
45
|
+
slug: config.project.name,
|
|
46
|
+
framework: config.project.framework,
|
|
47
|
+
});
|
|
48
|
+
project = res.project;
|
|
49
|
+
consola.success(`Created project: ${project.slug}`);
|
|
50
|
+
}
|
|
51
|
+
// Build
|
|
52
|
+
if (!args["skip-build"]) {
|
|
53
|
+
consola.start(`Building with: ${config.build.command}`);
|
|
54
|
+
try {
|
|
55
|
+
execSync(config.build.command, { cwd, stdio: "inherit" });
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
consola.error("Build failed");
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
consola.success("Build complete");
|
|
62
|
+
}
|
|
63
|
+
const outputDir = resolve(cwd, config.build.output);
|
|
64
|
+
if (!existsSync(outputDir)) {
|
|
65
|
+
consola.error(`Build output directory not found: ${config.build.output}`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
const framework = config.project.framework ?? null;
|
|
69
|
+
const isSSR = isSSRFramework(framework);
|
|
70
|
+
const renderMode = isSSR ? "ssr" : "spa";
|
|
71
|
+
// Collect client assets
|
|
72
|
+
let clientAssetsDir = outputDir;
|
|
73
|
+
if (isSSR && framework) {
|
|
74
|
+
const clientSubdir = getClientAssetsDir(framework);
|
|
75
|
+
if (clientSubdir) {
|
|
76
|
+
clientAssetsDir = resolve(outputDir, clientSubdir);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
consola.start("Collecting assets...");
|
|
80
|
+
const { assets: clientAssets, fileList } = collectAssets(clientAssetsDir);
|
|
81
|
+
consola.info(`Found ${fileList.length} client assets`);
|
|
82
|
+
// Bundle server files for SSR
|
|
83
|
+
let serverFiles;
|
|
84
|
+
if (isSSR && framework) {
|
|
85
|
+
const serverEntry = getSSRServerEntry(framework);
|
|
86
|
+
if (serverEntry) {
|
|
87
|
+
const serverEntryPath = resolve(outputDir, serverEntry);
|
|
88
|
+
if (existsSync(serverEntryPath)) {
|
|
89
|
+
consola.start("Bundling SSR server...");
|
|
90
|
+
const bundled = await bundleSSRServer(serverEntryPath);
|
|
91
|
+
// Send as base64-encoded single file
|
|
92
|
+
serverFiles = {
|
|
93
|
+
"server.js": Buffer.from(bundled).toString("base64"),
|
|
94
|
+
};
|
|
95
|
+
consola.success(`SSR server bundled (${Math.round(bundled.length / 1024)}KB)`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
// Create deployment
|
|
100
|
+
consola.start("Creating deployment...");
|
|
101
|
+
const { deployment } = await client.createDeployment(project.id);
|
|
102
|
+
// Upload bundle
|
|
103
|
+
consola.start("Uploading...");
|
|
104
|
+
const bundle = {
|
|
105
|
+
manifest: {
|
|
106
|
+
assets: fileList,
|
|
107
|
+
hasWorker: isSSR,
|
|
108
|
+
entrypoint: null,
|
|
109
|
+
renderMode,
|
|
110
|
+
},
|
|
111
|
+
workerScript: null,
|
|
112
|
+
assets: clientAssets,
|
|
113
|
+
serverFiles,
|
|
114
|
+
};
|
|
115
|
+
const result = await client.uploadDeploymentBundle(project.id, deployment.id, bundle);
|
|
116
|
+
if (result.url) {
|
|
117
|
+
consola.success(`Deployed! ${result.url}`);
|
|
118
|
+
if (result.previewUrl) {
|
|
119
|
+
consola.info(`Preview: ${result.previewUrl}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
let status = deployment.status;
|
|
124
|
+
let url = null;
|
|
125
|
+
for (let i = 0; i < 30; i++) {
|
|
126
|
+
const res = await client.getDeploymentStatus(project.id, deployment.id);
|
|
127
|
+
status = res.deployment.status;
|
|
128
|
+
url = res.url;
|
|
129
|
+
if (status === "active" || status === "failed")
|
|
130
|
+
break;
|
|
131
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
132
|
+
}
|
|
133
|
+
if (status === "active" && url) {
|
|
134
|
+
consola.success(`Deployed! ${url}`);
|
|
135
|
+
}
|
|
136
|
+
else if (status === "failed") {
|
|
137
|
+
consola.error("Deployment failed");
|
|
138
|
+
process.exit(1);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
consola.warn(`Deployment status: ${status}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
//# sourceMappingURL=deploy.js.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import consola from "consola";
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join, basename } from "node:path";
|
|
5
|
+
import { stringify } from "smol-toml";
|
|
6
|
+
import { detectFramework } from "@solcreek/sdk";
|
|
7
|
+
export const initCommand = defineCommand({
|
|
8
|
+
meta: {
|
|
9
|
+
name: "init",
|
|
10
|
+
description: "Initialize a new Creek project",
|
|
11
|
+
},
|
|
12
|
+
args: {
|
|
13
|
+
name: {
|
|
14
|
+
type: "string",
|
|
15
|
+
description: "Project name",
|
|
16
|
+
required: false,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
async run({ args }) {
|
|
20
|
+
const cwd = process.cwd();
|
|
21
|
+
const configPath = join(cwd, "creek.toml");
|
|
22
|
+
if (existsSync(configPath)) {
|
|
23
|
+
consola.warn("creek.toml already exists");
|
|
24
|
+
const overwrite = await consola.prompt("Overwrite?", { type: "confirm" });
|
|
25
|
+
if (!overwrite)
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
// Detect framework
|
|
29
|
+
const pkgPath = join(cwd, "package.json");
|
|
30
|
+
let framework;
|
|
31
|
+
if (existsSync(pkgPath)) {
|
|
32
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
33
|
+
const detected = detectFramework(pkg);
|
|
34
|
+
if (detected) {
|
|
35
|
+
framework = detected;
|
|
36
|
+
consola.info(`Detected framework: ${framework}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const defaultName = basename(cwd).toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
40
|
+
const name = args.name ?? defaultName;
|
|
41
|
+
const config = {
|
|
42
|
+
project: {
|
|
43
|
+
name,
|
|
44
|
+
...(framework ? { framework } : {}),
|
|
45
|
+
},
|
|
46
|
+
build: {
|
|
47
|
+
command: "npm run build",
|
|
48
|
+
output: "dist",
|
|
49
|
+
},
|
|
50
|
+
resources: {
|
|
51
|
+
d1: false,
|
|
52
|
+
kv: false,
|
|
53
|
+
r2: false,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
writeFileSync(configPath, stringify(config));
|
|
57
|
+
consola.success(`Created creek.toml for "${name}"`);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
//# sourceMappingURL=init.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
import consola from "consola";
|
|
3
|
+
import { writeCliConfig, readCliConfig } from "../utils/config.js";
|
|
4
|
+
export const loginCommand = defineCommand({
|
|
5
|
+
meta: {
|
|
6
|
+
name: "login",
|
|
7
|
+
description: "Authenticate with Creek using an API token",
|
|
8
|
+
},
|
|
9
|
+
args: {
|
|
10
|
+
token: {
|
|
11
|
+
type: "string",
|
|
12
|
+
description: "API token",
|
|
13
|
+
required: false,
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
async run({ args }) {
|
|
17
|
+
let token = args.token;
|
|
18
|
+
if (!token) {
|
|
19
|
+
token = await consola.prompt("Enter your API token:", {
|
|
20
|
+
type: "text",
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
if (!token || typeof token !== "string") {
|
|
24
|
+
consola.error("No token provided");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
const config = readCliConfig();
|
|
28
|
+
writeCliConfig({ ...config, token });
|
|
29
|
+
consola.success("Token saved successfully");
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
//# sourceMappingURL=login.js.map
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { defineCommand, runMain } from "citty";
|
|
3
|
+
import { loginCommand } from "./commands/login.js";
|
|
4
|
+
import { initCommand } from "./commands/init.js";
|
|
5
|
+
import { deployCommand } from "./commands/deploy.js";
|
|
6
|
+
const main = defineCommand({
|
|
7
|
+
meta: {
|
|
8
|
+
name: "creek",
|
|
9
|
+
version: "0.1.0",
|
|
10
|
+
description: "Deploy full-stack apps to the edge",
|
|
11
|
+
},
|
|
12
|
+
subCommands: {
|
|
13
|
+
login: loginCommand,
|
|
14
|
+
init: initCommand,
|
|
15
|
+
deploy: deployCommand,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
runMain(main);
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
export function collectAssets(dir, baseDir) {
|
|
4
|
+
const base = baseDir ?? dir;
|
|
5
|
+
const assets = {};
|
|
6
|
+
const fileList = [];
|
|
7
|
+
const entries = readdirSync(dir, { withFileTypes: true });
|
|
8
|
+
for (const entry of entries) {
|
|
9
|
+
const fullPath = join(dir, entry.name);
|
|
10
|
+
if (entry.isDirectory()) {
|
|
11
|
+
const sub = collectAssets(fullPath, base);
|
|
12
|
+
Object.assign(assets, sub.assets);
|
|
13
|
+
fileList.push(...sub.fileList);
|
|
14
|
+
}
|
|
15
|
+
else if (entry.isFile()) {
|
|
16
|
+
const relPath = relative(base, fullPath);
|
|
17
|
+
const content = readFileSync(fullPath);
|
|
18
|
+
assets[relPath] = content.toString("base64");
|
|
19
|
+
fileList.push(relPath);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return { assets, fileList };
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=bundle.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface CliConfig {
|
|
2
|
+
token?: string;
|
|
3
|
+
apiUrl?: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function getConfigDir(): string;
|
|
6
|
+
export declare function readCliConfig(): CliConfig;
|
|
7
|
+
export declare function writeCliConfig(config: CliConfig): void;
|
|
8
|
+
export declare function getToken(): string | undefined;
|
|
9
|
+
export declare function getApiUrl(): string;
|
|
10
|
+
export {};
|
|
11
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
const CONFIG_DIR = join(homedir(), ".creek");
|
|
5
|
+
const CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
6
|
+
export function getConfigDir() {
|
|
7
|
+
return CONFIG_DIR;
|
|
8
|
+
}
|
|
9
|
+
export function readCliConfig() {
|
|
10
|
+
if (!existsSync(CONFIG_FILE))
|
|
11
|
+
return {};
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function writeCliConfig(config) {
|
|
20
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
21
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
24
|
+
}
|
|
25
|
+
export function getToken() {
|
|
26
|
+
return process.env.CREEK_TOKEN ?? readCliConfig().token;
|
|
27
|
+
}
|
|
28
|
+
export function getApiUrl() {
|
|
29
|
+
return (process.env.CREEK_API_URL ??
|
|
30
|
+
readCliConfig().apiUrl ??
|
|
31
|
+
"https://api.creek.dev");
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle an SSR server entry point into a single standalone worker script.
|
|
3
|
+
* Uses esbuild (same bundler as wrangler) with node: builtins as externals.
|
|
4
|
+
*/
|
|
5
|
+
export declare function bundleSSRServer(entryPoint: string): Promise<string>;
|
|
6
|
+
//# sourceMappingURL=ssr-bundle.d.ts.map
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { build } from "esbuild";
|
|
2
|
+
/**
|
|
3
|
+
* Bundle an SSR server entry point into a single standalone worker script.
|
|
4
|
+
* Uses esbuild (same bundler as wrangler) with node: builtins as externals.
|
|
5
|
+
*/
|
|
6
|
+
export async function bundleSSRServer(entryPoint) {
|
|
7
|
+
const result = await build({
|
|
8
|
+
entryPoints: [entryPoint],
|
|
9
|
+
bundle: true,
|
|
10
|
+
format: "esm",
|
|
11
|
+
platform: "neutral",
|
|
12
|
+
target: "es2022",
|
|
13
|
+
write: false,
|
|
14
|
+
minify: false,
|
|
15
|
+
external: [
|
|
16
|
+
"node:async_hooks",
|
|
17
|
+
"node:stream",
|
|
18
|
+
"node:stream/web",
|
|
19
|
+
"node:buffer",
|
|
20
|
+
"node:util",
|
|
21
|
+
"node:events",
|
|
22
|
+
"node:crypto",
|
|
23
|
+
"node:path",
|
|
24
|
+
"node:url",
|
|
25
|
+
"node:string_decoder",
|
|
26
|
+
"node:diagnostics_channel",
|
|
27
|
+
"node:process",
|
|
28
|
+
"node:fs",
|
|
29
|
+
"node:os",
|
|
30
|
+
"node:child_process",
|
|
31
|
+
"node:http",
|
|
32
|
+
"node:https",
|
|
33
|
+
"node:net",
|
|
34
|
+
"node:tls",
|
|
35
|
+
"node:zlib",
|
|
36
|
+
"node:perf_hooks",
|
|
37
|
+
"node:worker_threads",
|
|
38
|
+
],
|
|
39
|
+
conditions: ["workerd", "worker", "import"],
|
|
40
|
+
mainFields: ["module", "main"],
|
|
41
|
+
logLevel: "warning",
|
|
42
|
+
});
|
|
43
|
+
if (result.errors.length > 0) {
|
|
44
|
+
throw new Error(`esbuild: ${result.errors.map((e) => e.text).join(", ")}`);
|
|
45
|
+
}
|
|
46
|
+
return result.outputFiles[0].text;
|
|
47
|
+
}
|
|
48
|
+
//# sourceMappingURL=ssr-bundle.js.map
|
package/package.json
CHANGED
|
@@ -1,24 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "creek",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
"
|
|
3
|
+
"version": "0.3.0-alpha.2",
|
|
4
|
+
"description": "CLI for the Creek deployment platform",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"creek": "./dist/index.js"
|
|
8
8
|
},
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"!dist/**/*.test.*",
|
|
12
|
+
"!dist/**/*.map"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"creek",
|
|
16
|
+
"cli",
|
|
17
|
+
"deployment",
|
|
18
|
+
"cloudflare",
|
|
19
|
+
"workers"
|
|
19
20
|
],
|
|
21
|
+
"author": "SolCreek",
|
|
22
|
+
"license": "Apache-2.0",
|
|
23
|
+
"homepage": "https://creek.dev",
|
|
20
24
|
"repository": {
|
|
21
25
|
"type": "git",
|
|
22
|
-
"url": "
|
|
26
|
+
"url": "https://github.com/solcreek/creek.git",
|
|
27
|
+
"directory": "packages/cli"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"citty": "^0.1.6",
|
|
31
|
+
"consola": "^3.4.2",
|
|
32
|
+
"smol-toml": "^1.3.1",
|
|
33
|
+
"esbuild": "^0.25.0",
|
|
34
|
+
"@solcreek/sdk": "0.1.0-alpha.1"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"typescript": "^5.8.2",
|
|
38
|
+
"@types/node": "^22.13.10"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsc",
|
|
42
|
+
"dev": "tsc --watch",
|
|
43
|
+
"typecheck": "tsc --noEmit",
|
|
44
|
+
"clean": "rm -rf dist"
|
|
23
45
|
}
|
|
24
46
|
}
|
package/README.md
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
When you've got a hosepipe, what you need is a Creek!
|
|
2
|
-
=====================================================
|
|
3
|
-
Creek is a dead simple way to run performant summary analysis over time series data. Creek is written in coffee-script and has only been tested with node 0.2.5.
|
|
4
|
-
|
|
5
|
-
Current Status
|
|
6
|
-
--------------
|
|
7
|
-
It's early days for the project and there may be some warts however Creek is currently used in production at Forward to analyse over 35 million messages per day.
|
|
8
|
-
|
|
9
|
-
The command line tool and configuration interface is fairly stable however the code level interfaces not so much.
|
|
10
|
-
|
|
11
|
-
Hello World Example
|
|
12
|
-
-------------------
|
|
13
|
-
|
|
14
|
-
Get the code with either `npm install creek` or clone this repo and either add creek to your path or use ./bin/creek in place.
|
|
15
|
-
|
|
16
|
-
then create a file somewhere called `hello.creek` containing the following...
|
|
17
|
-
|
|
18
|
-
parser 'words'
|
|
19
|
-
interface 'rest'
|
|
20
|
-
track 'unique-words', aggregator: distinct.alltime
|
|
21
|
-
track 'count', aggregator: count.alltime
|
|
22
|
-
|
|
23
|
-
Then run `echo 'hello world from creek' | creek hello.creek`
|
|
24
|
-
|
|
25
|
-
To see Creek in action run `curl "http://localhost:8080/"` from another window.
|
|
26
|
-
|
|
27
|
-
Twitter Example
|
|
28
|
-
---------------
|
|
29
|
-
|
|
30
|
-
As with the hello world example above but using this config instead...
|
|
31
|
-
|
|
32
|
-
parser 'json', seperatedBy: '\r'
|
|
33
|
-
interface 'rest'
|
|
34
|
-
|
|
35
|
-
track 'languages-seen'
|
|
36
|
-
aggregator: distinct.alltime
|
|
37
|
-
field: (o) -> if o.user then o.user.lang else undefined
|
|
38
|
-
|
|
39
|
-
track 'popular-words'
|
|
40
|
-
aggregator: popular.timeboxed
|
|
41
|
-
field: (o) -> if o.text then o.text.toLowerCase().split(' ') else undefined
|
|
42
|
-
period: 60
|
|
43
|
-
precision: 5
|
|
44
|
-
top: 10
|
|
45
|
-
before: (v) -> if v and v.length > 4 then v else undefined
|
|
46
|
-
|
|
47
|
-
track 'popular-urls'
|
|
48
|
-
aggregator: popular.timeboxed
|
|
49
|
-
field: (o) -> if o.text then o.text.split(' ') else undefined
|
|
50
|
-
period: 60*30
|
|
51
|
-
precision: 60
|
|
52
|
-
top: 5
|
|
53
|
-
before: (v) -> if v and v.indexOf('http://') is 0 then v else undefined
|
|
54
|
-
|
|
55
|
-
Then run `curl http://stream.twitter.com/1/statuses/sample.json -u USERNAME:PASSWORD | creek twitter.creek` and visit `http://localhost:8080/`
|
|
56
|
-
|
|
57
|
-
Parsers
|
|
58
|
-
-------
|
|
59
|
-
You must choose a parser, the currently available options are...
|
|
60
|
-
|
|
61
|
-
* words - this pushes each word to the aggregator using the current timestamp
|
|
62
|
-
* json - expects line separated JSON objects
|
|
63
|
-
* chunked - this is a generic parser which can chunk a stream based on any string or regex
|
|
64
|
-
* zeromq - allows subscription to a zeromq channel rather than input on stdin.
|
|
65
|
-
|
|
66
|
-
Currently all the parsers expect utf8 or a subset thereof.
|
|
67
|
-
|
|
68
|
-
Interfaces
|
|
69
|
-
----------
|
|
70
|
-
There is currently only one interface available...
|
|
71
|
-
|
|
72
|
-
* rest - this makes a JSON rest api available running on localhost:8080 by default.
|
|
73
|
-
|
|
74
|
-
Aggregators
|
|
75
|
-
-----------
|
|
76
|
-
The currently available aggregators are...
|
|
77
|
-
|
|
78
|
-
* count.alltime
|
|
79
|
-
* count.timeboxed
|
|
80
|
-
* distinct.alltime
|
|
81
|
-
* distinct.timeboxed
|
|
82
|
-
* max.alltime
|
|
83
|
-
* max.timeboxed
|
|
84
|
-
* mean.alltime
|
|
85
|
-
* mean.timeboxed
|
|
86
|
-
* min.alltime
|
|
87
|
-
* min.timeboxed
|
|
88
|
-
* popular.timeboxed
|
|
89
|
-
* recent.limited
|
|
90
|
-
* sum.alltime
|
|
91
|
-
* sum.timeboxed
|
|
92
|
-
|
|
93
|
-
All aggregators support `field` and `before` options and timeboxed ones also support `period` and `precision` settings.
|
|
94
|
-
|
|
95
|
-
* field - defaults to the whole object, can be a string in which case it uses that key on the object, or an integer in which case it uses that as a constant value, or a function which takes an object and returns a value to be used.
|
|
96
|
-
* before - a function that takes each chunks field right before it is about to be pushed to the aggregator, you should return the original value, a modified value or undefined if you would like this chunk to be skipped.
|
|
97
|
-
* period - this is the period in seconds over which you would like the timeboxed aggregation to run. A value of 60 will keep track of the value over the last rolling 1 min window. The default value is 60 seconds.
|
|
98
|
-
* precision - the accuracy of the rolling time window specified in seconds. I general lower the value used here the more memory will be required. The default value is 1 second.
|
|
99
|
-
|
|
100
|
-
Notes
|
|
101
|
-
-----
|
|
102
|
-
Please note that the current aggregator implementations are fairly immature and they may not be optimal in terms of RAM or CPU usage at this point.
|
|
103
|
-
What I can say though is they are efficient enough for most use cases and Creek can comfortably handle dozens of aggregators running across 1,000s of records per second.
|
|
104
|
-
|
|
105
|
-
If anyone would like to contribute interfaces, parsers or aggregators they would be gladly received.
|
package/bin/creek
DELETED
package/examples/hello.creek
DELETED
package/examples/twitter.creek
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
parser 'json', seperatedBy: '\r'
|
|
2
|
-
interface 'rest'
|
|
3
|
-
|
|
4
|
-
track 'languages-seen',
|
|
5
|
-
aggregator: distinct.alltime
|
|
6
|
-
field: (o) -> if o.user then o.user.lang else undefined
|
|
7
|
-
|
|
8
|
-
track 'popular-words',
|
|
9
|
-
aggregator: popular.timeboxed
|
|
10
|
-
field: (o) -> if o.text then o.text.toLowerCase().split(' ') else undefined
|
|
11
|
-
period: 60
|
|
12
|
-
precision: 5
|
|
13
|
-
top: 10
|
|
14
|
-
keep: 5
|
|
15
|
-
before: (v) -> if v and v.length > 4 then v else undefined
|
|
16
|
-
|
|
17
|
-
track 'popular-urls',
|
|
18
|
-
aggregator: popular.timeboxed
|
|
19
|
-
field: (o) -> if o.text then o.text.split(' ') else undefined
|
|
20
|
-
period: 60*30
|
|
21
|
-
precision: 60
|
|
22
|
-
top: 5
|
|
23
|
-
before: (v) -> if v and v.indexOf('http://') is 0 then v else undefined
|
package/examples/zeromq.creek
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
parser 'zeromq',
|
|
2
|
-
url: 'tcp://example:5555',
|
|
3
|
-
channel: 'redirect',
|
|
4
|
-
as: 'json',
|
|
5
|
-
bucketedBy: 'site',
|
|
6
|
-
timestampedBy:'timestamp'
|
|
7
|
-
|
|
8
|
-
interface 'rest'
|
|
9
|
-
|
|
10
|
-
track 'count', aggregator: count.alltime
|
|
11
|
-
track 'popular-keywords',
|
|
12
|
-
aggregator: popular.timeboxed
|
|
13
|
-
field: (o) -> unescape(o.keyword)
|
|
14
|
-
period: 60
|
|
15
|
-
precision: 5
|
|
16
|
-
top: 10
|
package/lib/aggregator.coffee
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
events = require('events')
|
|
2
|
-
|
|
3
|
-
class Aggregator
|
|
4
|
-
constructor: (name, implementation, opts) ->
|
|
5
|
-
@name = name
|
|
6
|
-
@implementation = implementation
|
|
7
|
-
@opts = opts
|
|
8
|
-
@implementation.init.call(this, @opts)
|
|
9
|
-
@cachedValue = null
|
|
10
|
-
@events = new events.EventEmitter()
|
|
11
|
-
on: (event, callback) ->
|
|
12
|
-
@events.on(event, callback)
|
|
13
|
-
push: (time, values) ->
|
|
14
|
-
values = [values] unless Array.isArray(values)
|
|
15
|
-
for value in values
|
|
16
|
-
value = @opts.before.call(this, value) if @opts.before
|
|
17
|
-
@implementation.push.call(this, time, value) unless value is undefined
|
|
18
|
-
oldValue = @cachedValue
|
|
19
|
-
@events.emit('change', @cachedValue, oldValue) unless @compute() is oldValue
|
|
20
|
-
compute: ->
|
|
21
|
-
@cachedValue = @implementation.compute.call(this)
|
|
22
|
-
@cachedValue = @opts.after.call(this, @cachedValue) if @opts.after
|
|
23
|
-
@cachedValue
|
|
24
|
-
value: ->
|
|
25
|
-
@cachedValue
|
|
26
|
-
|
|
27
|
-
exports.buildAggregator = (name, implementation)-> ((opts) -> new Aggregator(name, implementation, opts))
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
aggregator = require('../aggregator')
|
|
2
|
-
timeboxedAggregator = require('../timeboxed-aggregator')
|
|
3
|
-
|
|
4
|
-
exports.alltime = aggregator.buildAggregator 'Alltime Count',
|
|
5
|
-
init: (opts) -> @count = 0
|
|
6
|
-
push: (time, value) -> @count++
|
|
7
|
-
compute: -> @count
|
|
8
|
-
|
|
9
|
-
exports.timeboxed = timeboxedAggregator.buildTimeboxedAggregator 'Timeboxed Count',
|
|
10
|
-
recalculateBlockData: (blockData, value) ->
|
|
11
|
-
blockData + 1
|
|
12
|
-
computeFromBlocks: (blocks) ->
|
|
13
|
-
count = 0
|
|
14
|
-
count += block.data for block in blocks
|
|
15
|
-
count
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
aggregator = require('../aggregator')
|
|
2
|
-
timeboxedAggregator = require('../timeboxed-aggregator')
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
exports.alltime = aggregator.buildAggregator 'Alltime Distinct',
|
|
6
|
-
init: (opts) -> @records = []
|
|
7
|
-
push: (time, value) -> @records.push(value) if @records.indexOf(value) is -1
|
|
8
|
-
compute: -> @records
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
exports.timeboxed = timeboxedAggregator.buildTimeboxedAggregator 'Timeboxed Distinct',
|
|
12
|
-
recalculateBlockData: (blockData, value) ->
|
|
13
|
-
blockData += value
|
|
14
|
-
blockData.push(value) if blockData.indexOf(value) is -1
|
|
15
|
-
computeFromBlocks: (blocks) ->
|
|
16
|
-
uniques = []
|
|
17
|
-
for block in blocks
|
|
18
|
-
for value in block
|
|
19
|
-
uniques.push(value) if uniques.indexOf(value) is -1
|
|
20
|
-
uniques
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
aggregator = require('../aggregator')
|
|
2
|
-
timeboxedAggregator = require('../timeboxed-aggregator')
|
|
3
|
-
|
|
4
|
-
exports.alltime = aggregator.buildAggregator 'Alltime Maximum',
|
|
5
|
-
init: (opts) -> @max = null
|
|
6
|
-
push: (time, value) -> @max = value if @max == null or @max < value
|
|
7
|
-
compute: -> @max
|
|
8
|
-
|
|
9
|
-
exports.timeboxed = timeboxedAggregator.buildTimeboxedAggregator 'Timeboxed Maximum',
|
|
10
|
-
recalculateBlockData: (blockData, value) ->
|
|
11
|
-
if blockData is null or blockData < value then value else blockData
|
|
12
|
-
computeFromBlocks: (blocks) ->
|
|
13
|
-
max = null
|
|
14
|
-
for block in blocks
|
|
15
|
-
max = block.data if max == null or max < block.data
|
|
16
|
-
max
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
aggregator = require('../aggregator')
|
|
2
|
-
timeboxedAggregator = require('../timeboxed-aggregator')
|
|
3
|
-
|
|
4
|
-
exports.alltime = aggregator.buildAggregator 'Alltime Mean',
|
|
5
|
-
init: (opts) ->
|
|
6
|
-
@count = 0
|
|
7
|
-
@total = 0
|
|
8
|
-
push: (time, value) ->
|
|
9
|
-
@count++
|
|
10
|
-
@total += value
|
|
11
|
-
compute: -> @total / @count
|
|
12
|
-
|
|
13
|
-
exports.timeboxed = timeboxedAggregator.buildTimeboxedAggregator 'Timeboxed Mean',
|
|
14
|
-
recalculateBlockData: (blockData, value) ->
|
|
15
|
-
blockData.count++
|
|
16
|
-
blockData.total += value
|
|
17
|
-
blockData
|
|
18
|
-
defaultBlockValue: ->
|
|
19
|
-
{total:0, count:0}
|
|
20
|
-
computeFromBlocks: (blocks) ->
|
|
21
|
-
total = 0
|
|
22
|
-
count = 0
|
|
23
|
-
for block in blocks
|
|
24
|
-
total += block.data.total
|
|
25
|
-
count += block.data.count
|
|
26
|
-
return 0 if count is 0
|
|
27
|
-
total / count
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
aggregator = require('../aggregator')
|
|
2
|
-
timeboxedAggregator = require('../timeboxed-aggregator')
|
|
3
|
-
|
|
4
|
-
exports.alltime = aggregator.buildAggregator 'Alltime Minimum',
|
|
5
|
-
init: (opts) -> @max = null
|
|
6
|
-
push: (time, value) -> @max = value if @max == null or @max > value
|
|
7
|
-
compute: -> @max
|
|
8
|
-
|
|
9
|
-
exports.timeboxed = timeboxedAggregator.buildTimeboxedAggregator 'Timeboxed Minimum',
|
|
10
|
-
recalculateBlockData: (blockData, value) ->
|
|
11
|
-
if blockData is null or blockData > value then value else blockData
|
|
12
|
-
computeFromBlocks: (blocks) ->
|
|
13
|
-
min = null
|
|
14
|
-
for block in blocks
|
|
15
|
-
min = block.data if min == null or min > block.data
|
|
16
|
-
min
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
timeboxedAggregator = require('../timeboxed-aggregator')
|
|
2
|
-
|
|
3
|
-
exports.timeboxed = timeboxedAggregator.buildTimeboxedAggregator 'Timeboxed Popular',
|
|
4
|
-
init: (opts) ->
|
|
5
|
-
@numberOfResultsToKeep = opts.top
|
|
6
|
-
@numberOfResultsToKeep = 10 if @numberOfResultsToKeep is null
|
|
7
|
-
defaultBlockValue: () -> {}
|
|
8
|
-
closeBlock: (block) ->
|
|
9
|
-
values = Object.keys(block.data)
|
|
10
|
-
topValues = values.sort((a,b) -> block.data[b] - block.data[a])
|
|
11
|
-
topValues = topValues.slice(0, @numberOfResultsToKeep) if numberOfResultsToKeep?
|
|
12
|
-
topValues.reduce ((m,v) -> m[v] = block.data[v]; m), {}
|
|
13
|
-
recalculateBlockData: (blockData, value) ->
|
|
14
|
-
blockData[value] ?= 0
|
|
15
|
-
blockData[value]++
|
|
16
|
-
blockData
|
|
17
|
-
computeFromBlocks: (blocks) ->
|
|
18
|
-
completeBlocks = blocks.slice(0,blocks.length-1)
|
|
19
|
-
foldBlocksTogether = (m,v) ->
|
|
20
|
-
for value, count of v.data
|
|
21
|
-
m[value] = 0 unless m[value]?
|
|
22
|
-
m[value] += count
|
|
23
|
-
m
|
|
24
|
-
foldedResults = completeBlocks.reduce foldBlocksTogether, {}
|
|
25
|
-
topValues = Object.keys(foldedResults).sort((a,b) -> foldedResults[b] - foldedResults[a])
|
|
26
|
-
topValues = topValues.slice(0, @numberOfResultsToKeep) if @numberOfResultsToKeep?
|
|
27
|
-
topValues.reduce ((m,v) -> m.push(value:v, count:foldedResults[v]); m), []
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
aggregator = require('../aggregator')
|
|
2
|
-
|
|
3
|
-
exports.limited = aggregator.buildAggregator 'Most Recent',
|
|
4
|
-
init: (opts) ->
|
|
5
|
-
@count = opts.count or 10
|
|
6
|
-
@items = []
|
|
7
|
-
push: (time, value) ->
|
|
8
|
-
@items.unshift(value)
|
|
9
|
-
@items.pop() if @items.length > @count
|
|
10
|
-
compute: -> @items
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
aggregator = require('../aggregator')
|
|
2
|
-
timeboxedAggregator = require('../timeboxed-aggregator')
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
exports.alltime = aggregator.buildAggregator 'Alltime Sum',
|
|
6
|
-
init: (opts) -> @total = 0
|
|
7
|
-
push: (time, value) -> @total += value
|
|
8
|
-
compute: -> @total
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
exports.timeboxed = timeboxedAggregator.buildTimeboxedAggregator 'Timeboxed Sum',
|
|
12
|
-
recalculateBlockData: (blockData, value) ->
|
|
13
|
-
blockData += value
|
|
14
|
-
computeFromBlocks: (blocks) ->
|
|
15
|
-
total = 0
|
|
16
|
-
total += block.data for block in blocks
|
|
17
|
-
total
|
package/lib/bootstrap.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
class CompoundAggregator
|
|
2
|
-
constructor: ->
|
|
3
|
-
@aggregators = {}
|
|
4
|
-
track: (name, opts) ->
|
|
5
|
-
getValue = switch typeof opts.field
|
|
6
|
-
when 'string' then ((o) -> o[opts.field])
|
|
7
|
-
when 'number' then ((o) -> opts.field)
|
|
8
|
-
when 'function' then ((o) -> opts.field(o))
|
|
9
|
-
else ((o) -> o)
|
|
10
|
-
@aggregators[name] = {getValue:getValue, aggregator:opts.aggregator(opts)}
|
|
11
|
-
console.log("Tracking '#{@aggregators[name].aggregator.name}' as '#{name}' with #{JSON.stringify(opts)}")
|
|
12
|
-
on: (name, event, callback) ->
|
|
13
|
-
if arguments.length == 3
|
|
14
|
-
@aggregators[name].aggregator.on(event, callback)
|
|
15
|
-
else
|
|
16
|
-
event = name
|
|
17
|
-
callback = event
|
|
18
|
-
agg.aggregator.on(event, callback) for name, agg of @aggregators
|
|
19
|
-
push: (time, obj) ->
|
|
20
|
-
for name, agg of @aggregators
|
|
21
|
-
val = agg.getValue(obj)
|
|
22
|
-
agg.aggregator.push(time, val)
|
|
23
|
-
value: (name) ->
|
|
24
|
-
if typeof name is 'string'
|
|
25
|
-
console.log("Aggregation '#{name}' does not exist!") unless @aggregators[name]
|
|
26
|
-
@aggregators[name].aggregator.value()
|
|
27
|
-
else
|
|
28
|
-
ret = {}
|
|
29
|
-
for name, agg of @aggregators
|
|
30
|
-
ret[name] = agg.aggregator.value()
|
|
31
|
-
ret
|
|
32
|
-
|
|
33
|
-
exports.create = -> new CompoundAggregator()
|
package/lib/creek.coffee
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
exports.aggregator = require('./aggregator')
|
|
2
|
-
exports.compoundAggregator = require('./compound-aggregator')
|
|
3
|
-
exports.lazyBucketedAggregator = require('./lazy-bucketed-aggregator')
|
|
4
|
-
|
|
5
|
-
exports.aggregators =
|
|
6
|
-
count: require('./aggregators/count')
|
|
7
|
-
distinct: require('./aggregators/distinct')
|
|
8
|
-
max: require('./aggregators/max')
|
|
9
|
-
mean: require('./aggregators/mean')
|
|
10
|
-
min: require('./aggregators/min')
|
|
11
|
-
popular: require('./aggregators/popular')
|
|
12
|
-
recent: require('./aggregators/recent')
|
|
13
|
-
sum: require('./aggregators/sum')
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
http = require('http')
|
|
2
|
-
url = require('url')
|
|
3
|
-
|
|
4
|
-
exports.init = (agg, opts) ->
|
|
5
|
-
console.log("Rest Server Started")
|
|
6
|
-
server = http.createServer (req, res) ->
|
|
7
|
-
path = url.parse(req.url).pathname
|
|
8
|
-
path = path.replace(opts.path, '') if opts.path
|
|
9
|
-
pathParts = path.split('/')
|
|
10
|
-
pathParts.shift()
|
|
11
|
-
buildResponse = (obj) ->
|
|
12
|
-
callback = url.parse(req.url, true).query?.callback
|
|
13
|
-
if callback?
|
|
14
|
-
"#{callback}(#{JSON.stringify(obj)});"
|
|
15
|
-
else
|
|
16
|
-
JSON.stringify(obj)
|
|
17
|
-
try
|
|
18
|
-
result = if pathParts.length is 0
|
|
19
|
-
buildResponse(agg.value())
|
|
20
|
-
else if pathParts.length is 1
|
|
21
|
-
buildResponse(agg.value(null, pathParts[0]))
|
|
22
|
-
else
|
|
23
|
-
buildResponse(agg.value(pathParts[1], pathParts[0]))
|
|
24
|
-
catch e
|
|
25
|
-
res.writeHead(500, 'Content-Type': 'application/json; charset=utf-8')
|
|
26
|
-
res.end(buildResponse(message: "Unable to display requested data"))
|
|
27
|
-
return
|
|
28
|
-
res.writeHead(200, 'Content-Type': 'application/json')
|
|
29
|
-
res.end(result)
|
|
30
|
-
server.listen (opts.port or 8080), (opts.host or 'localhost')
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
http = require('http')
|
|
2
|
-
io = require('socket.io')
|
|
3
|
-
|
|
4
|
-
exports.init = (agg, opts) ->
|
|
5
|
-
server = http.createServer (req, res) ->
|
|
6
|
-
res.writeHead(200, {'Content-Type': 'text/html'})
|
|
7
|
-
res.write('<h1>Hello world</h1>')
|
|
8
|
-
res.end()
|
|
9
|
-
|
|
10
|
-
server.listen(8888)
|
|
11
|
-
|
|
12
|
-
socket = io.listen(server)
|
|
13
|
-
|
|
14
|
-
agg.on 'change', (newValue) ->
|
|
15
|
-
socket.broadcast(JSON.stringify(newValue))
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
compoundAggregator = require('./compound-aggregator')
|
|
2
|
-
|
|
3
|
-
class LazyBucketedAggregator
|
|
4
|
-
constructor: ->
|
|
5
|
-
@aggregatorOpts = {}
|
|
6
|
-
@aggregators = {}
|
|
7
|
-
@events = []
|
|
8
|
-
track: (name, opts) ->
|
|
9
|
-
@aggregatorOpts[name] = opts
|
|
10
|
-
console.log("Lazy Tracking '#{name}' with #{JSON.stringify(opts)}")
|
|
11
|
-
on: (name, event, callback) ->
|
|
12
|
-
if arguments.length == 2
|
|
13
|
-
event = name
|
|
14
|
-
callback = event
|
|
15
|
-
name = null
|
|
16
|
-
@events.push( name:name, event:event, callback:callback )
|
|
17
|
-
push: (bucket, time, obj) ->
|
|
18
|
-
unless @aggregators[bucket]
|
|
19
|
-
@aggregators[bucket] = compoundAggregator.create()
|
|
20
|
-
@aggregators[bucket].track(name, opts) for name, opts of @aggregatorOpts
|
|
21
|
-
for e in @events
|
|
22
|
-
@aggregators[bucket].on e.name, e.event, (newValue, oldValue) -> e.callback(bucket, newValue, oldValue)
|
|
23
|
-
@aggregators[bucket].push(time, obj)
|
|
24
|
-
value: (name, bucket) ->
|
|
25
|
-
if bucket
|
|
26
|
-
@aggregators[bucket].value(name)
|
|
27
|
-
else
|
|
28
|
-
ret = {}
|
|
29
|
-
for bucket, agg of @aggregators
|
|
30
|
-
ret[bucket] = agg.value(name)
|
|
31
|
-
ret
|
|
32
|
-
buckets: ->
|
|
33
|
-
Object.keys(@aggregators)
|
|
34
|
-
|
|
35
|
-
exports.create = -> new LazyBucketedAggregator()
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
class ChunkedStream
|
|
2
|
-
constructor: (opts, recordHandler) ->
|
|
3
|
-
@seperator = opts.seperatedBy or "\n"
|
|
4
|
-
@stream = process.openStdin()
|
|
5
|
-
@stream.setEncoding('utf8')
|
|
6
|
-
@patialData = ""
|
|
7
|
-
@recordHandler = recordHandler
|
|
8
|
-
@stream.on 'data', @dataHandler
|
|
9
|
-
dataHandler: (data) =>
|
|
10
|
-
@patialData += data
|
|
11
|
-
parts = @patialData.split(@seperator)
|
|
12
|
-
@patialData = parts.pop() # last part will be an incomplete chunk or an empty string if we are lucky
|
|
13
|
-
for chunk in parts
|
|
14
|
-
@recordHandler(chunk) if chunk
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
exports.init = (agg, opts, handler) -> new ChunkedStream(opts, handler)
|
package/lib/parsers/json.coffee
DELETED
package/lib/parsers/words.coffee
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
zeromq = require('zeromq')
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
exports.init = (agg, opts, handler) ->
|
|
5
|
-
socket = zeromq.createSocket('sub')
|
|
6
|
-
socket.connect(opts.url)
|
|
7
|
-
socket.subscribe(opts.channel) if opts.channel
|
|
8
|
-
socket.on 'message', (ch, data) ->
|
|
9
|
-
utf8Data = data.toString('utf8')
|
|
10
|
-
if opts.as is 'json'
|
|
11
|
-
try
|
|
12
|
-
handler(JSON.parse(utf8Data))
|
|
13
|
-
catch e
|
|
14
|
-
console.log("ERR: failed to JSON parse - #{utf8Data}")
|
|
15
|
-
else
|
|
16
|
-
handler(utf8Data)
|
package/lib/runner.coffee
DELETED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
creek = require('./creek')
|
|
2
|
-
|
|
3
|
-
agg = creek.lazyBucketedAggregator.create()
|
|
4
|
-
|
|
5
|
-
publicInterface = {}
|
|
6
|
-
|
|
7
|
-
publicInterface.track = (name, opts) -> agg.track(name, opts)
|
|
8
|
-
|
|
9
|
-
for name, code of creek.aggregators
|
|
10
|
-
publicInterface[name] = code
|
|
11
|
-
|
|
12
|
-
publicInterface.interface = (interfaceName, opts) ->
|
|
13
|
-
opts ?= {}
|
|
14
|
-
require("./interfaces/#{interfaceName}").init(agg, opts)
|
|
15
|
-
|
|
16
|
-
publicInterface.parser = (parserName, opts) ->
|
|
17
|
-
opts ?= {}
|
|
18
|
-
require("./parsers/#{parserName}").init agg, opts, (record) ->
|
|
19
|
-
bucket = if opts.bucketedBy?
|
|
20
|
-
if typeof opts.bucketedBy is 'string'
|
|
21
|
-
record[opts.bucketedBy]
|
|
22
|
-
else
|
|
23
|
-
opts.bucketedBy(record)
|
|
24
|
-
else
|
|
25
|
-
null
|
|
26
|
-
timestamp = if opts.timestampedBy?
|
|
27
|
-
if typeof opts.timestampedBy is 'string'
|
|
28
|
-
new Date(record[opts.timestampedBy])
|
|
29
|
-
else
|
|
30
|
-
opts.timestampedBy(record)
|
|
31
|
-
else
|
|
32
|
-
new Date()
|
|
33
|
-
agg.push((bucket or 'default'), timestamp, record)
|
|
34
|
-
|
|
35
|
-
exports.boot = (configFile) ->
|
|
36
|
-
Script = process.binding('evals').Script
|
|
37
|
-
code = require('fs').readFileSync(configFile, 'utf8')
|
|
38
|
-
jsCode = require('coffee-script').compile(code)
|
|
39
|
-
Script.runInNewContext(jsCode, publicInterface, configFile)
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
events = require('events')
|
|
2
|
-
|
|
3
|
-
class TimeboxedAggregator
|
|
4
|
-
constructor: (name, implementation, opts) ->
|
|
5
|
-
@name = name
|
|
6
|
-
@opts = opts
|
|
7
|
-
@implementation = implementation
|
|
8
|
-
@period = (opts.period or 60) * 1000
|
|
9
|
-
@precision = (opts.precision or 1) * 1000
|
|
10
|
-
@historySize = opts.keep or 1
|
|
11
|
-
@history = []
|
|
12
|
-
@cachedValue = null
|
|
13
|
-
@blocks = []
|
|
14
|
-
@implementation.init.call(this, opts) if @implementation.init
|
|
15
|
-
@events = new events.EventEmitter()
|
|
16
|
-
@staleCache = false
|
|
17
|
-
setInterval (=> @compute(true)), @precision
|
|
18
|
-
on: (event, callback) ->
|
|
19
|
-
@events.on(event, callback)
|
|
20
|
-
push: (time, values) ->
|
|
21
|
-
currentBlock = @maybeCreateNewBlock(time)
|
|
22
|
-
values = [values] unless Array.isArray(values)
|
|
23
|
-
for value in values
|
|
24
|
-
value = @opts.before.call(this, value) if @opts.before
|
|
25
|
-
currentBlock.data = @implementation.recalculateBlockData.call(this, currentBlock.data, value, currentBlock.time, time) unless value is undefined
|
|
26
|
-
oldValue = @cachedValue
|
|
27
|
-
@compute()
|
|
28
|
-
@events.emit('change', @cachedValue, oldValue) if @cachedValue != oldValue
|
|
29
|
-
compute: (force=false) ->
|
|
30
|
-
@cleanup()
|
|
31
|
-
if force or @staleCache
|
|
32
|
-
@cachedValue = @implementation.computeFromBlocks.call(this, @blocks)
|
|
33
|
-
@cachedValue = @opts.after.call(this, @cachedValue) if @opts.after
|
|
34
|
-
@updateHistory()
|
|
35
|
-
@staleCache = false
|
|
36
|
-
@cachedValue
|
|
37
|
-
value: ->
|
|
38
|
-
@cachedValue
|
|
39
|
-
maybeCreateNewBlock: (time) ->
|
|
40
|
-
if @blocks.length == 0
|
|
41
|
-
@blocks.push( time:time, data:@defaultBlockValue() )
|
|
42
|
-
return @blocks[@blocks.length-1]
|
|
43
|
-
lastBlock = @blocks[@blocks.length-1]
|
|
44
|
-
diff = time - lastBlock.time.getTime()
|
|
45
|
-
if diff > @precision
|
|
46
|
-
@blocks.push( time:time, data:@defaultBlockValue() )
|
|
47
|
-
@blocks[@blocks.length-2].data = @implementation.closeBlock(lastBlock) if @implementation.closeBlock
|
|
48
|
-
@staleCache = true
|
|
49
|
-
@blocks[@blocks.length-1]
|
|
50
|
-
cleanup: ->
|
|
51
|
-
periodThreshold = new Date().getTime() - @period
|
|
52
|
-
loop
|
|
53
|
-
break if @blocks.length == 0 or @blocks[0].time.getTime() > periodThreshold
|
|
54
|
-
@blocks.shift()
|
|
55
|
-
defaultBlockValue: ->
|
|
56
|
-
if @implementation.defaultBlockValue then @implementation.defaultBlockValue.call(this) else null
|
|
57
|
-
updateHistory: ->
|
|
58
|
-
@history.unshift(@cachedValue)
|
|
59
|
-
@history.pop() if @history.length > @historySize
|
|
60
|
-
@history
|
|
61
|
-
|
|
62
|
-
exports.TimeboxedAggregator = TimeboxedAggregator
|
|
63
|
-
|
|
64
|
-
exports.buildTimeboxedAggregator = (name, implementation)-> ((opts) -> new TimeboxedAggregator(name, implementation, opts))
|